diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 8b35e5e..705fe54 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -28,7 +28,6 @@ jobs:
- name: Install dependencies
run: |
pip install -e .[testing]
- pip install pytest
- name: Run test suite
run: |
diff --git a/mjml/lib/tests/dict_merger_test.py b/mjml/lib/tests/dict_merger_test.py
index b2140ed..9147e03 100644
--- a/mjml/lib/tests/dict_merger_test.py
+++ b/mjml/lib/tests/dict_merger_test.py
@@ -2,23 +2,20 @@
# Copyright 2015 Felix Schwarz
# The source code in this file is licensed under the MIT license.
-from pythonic_testcase import *
-
from ..dict_merger import merge_dicts
-class DictMergerTest(PythonicTestCase):
- def test_returns_single_dict_unmodified(self):
- assert_equals({}, merge_dicts({}))
- assert_equals({'bar': 42}, merge_dicts({'bar': 42}))
+def test_returns_single_dict_unmodified():
+ assert merge_dicts({}) == {}
+ assert merge_dicts({'bar': 42}) == {'bar': 42}
- def test_can_merge_two_dicts_without_modifying_inputs(self):
- a = {'a': 1}
- b = {'b': 2}
- assert_equals({'a': 1, 'b': 2}, merge_dicts(a, b))
+def test_can_merge_two_dicts_without_modifying_inputs():
+ a = {'a': 1}
+ b = {'b': 2}
+ assert merge_dicts(a, b) == {'a': 1, 'b': 2}
- def test_can_merge_three_dicts_without_modifying_inputs(self):
- a = {'a': 1}
- b = {'b': 2}
- c = {'c': 3}
- assert_equals({'a': 1, 'b': 2, 'c': 3}, merge_dicts(a, b, c))
+def test_can_merge_three_dicts_without_modifying_inputs():
+ a = {'a': 1}
+ b = {'b': 2}
+ c = {'c': 3}
+ assert merge_dicts(a, b, c) == {'a': 1, 'b': 2, 'c': 3}
diff --git a/setup.cfg b/setup.cfg
index 8ce75c0..e77a6bd 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -54,11 +54,10 @@ exclude =
[options.extras_require]
testing =
- ddt
FakeFSHelpers
HTMLCompare >= 0.3.0 # >= 0.3.0: ability to ignore attribute ordering in HTML
lxml
- PythonicTestcase
+ pytest
css_inlining =
css_inline >= 0.11, < 0.12 # >= 0.11, < 0.12: CSSInliner(inline_style_tags=..., keep_link_tags=..., keep_style_tags=...)
diff --git a/tests/border_parser_test.py b/tests/border_parser_test.py
index 4fb9d6a..66b1b5b 100644
--- a/tests/border_parser_test.py
+++ b/tests/border_parser_test.py
@@ -1,9 +1,6 @@
-from unittest import TestCase
-
from mjml.helpers import borderParser
-class BorderParserTest(TestCase):
- def test_can_parse_css_none(self):
- self.assertEqual(0, borderParser('none'))
+def test_can_parse_css_none():
+ assert borderParser('none') == 0
diff --git a/tests/custom_components_test.py b/tests/custom_components_test.py
index 333c566..43494c8 100644
--- a/tests/custom_components_test.py
+++ b/tests/custom_components_test.py
@@ -1,6 +1,4 @@
-from unittest import TestCase
-
from htmlcompare import assert_same_html
from mjml import mjml_to_html
@@ -33,12 +31,11 @@ def render(self):
return f'
***
{content}***
'
-class CustomComponentsTest(TestCase):
- def test_custom_components(self):
- expected_html = load_expected_html('_custom')
- with get_mjml_fp('_custom') as mjml_fp:
- result_list = mjml_to_html(mjml_fp, custom_components=[MjTextCustom, MjTextOverride])
+def test_custom_components():
+ expected_html = load_expected_html('_custom')
+ with get_mjml_fp('_custom') as mjml_fp:
+ result_list = mjml_to_html(mjml_fp, custom_components=[MjTextCustom, MjTextOverride])
- assert not result_list.errors
- list_actual_html = result_list.html
- assert_same_html(expected_html, list_actual_html, verbose=True)
+ assert not result_list.errors
+ list_actual_html = result_list.html
+ assert_same_html(expected_html, list_actual_html, verbose=True)
diff --git a/tests/includes_with_umlauts_test.py b/tests/includes_with_umlauts_test.py
index 3230d12..e97bd29 100644
--- a/tests/includes_with_umlauts_test.py
+++ b/tests/includes_with_umlauts_test.py
@@ -1,33 +1,38 @@
from io import StringIO
-from unittest import TestCase
+import pytest
from schwarz.fakefs_helpers import FakeFS
from mjml import mjml_to_html
-class IncludesWithUmlautsTest(TestCase):
- def test_can_properly_handle_include_umlauts(self):
- fs = FakeFS.set_up(test=self)
- included_mjml = (
- ''
- ' '
- ' äöüß'
- ' '
- ''
- )
- mjml = (
- ''
- ' '
- ' foo bar'
- ' '
- ' '
- ''
- )
- fs.create_file('footer.mjml', contents=included_mjml.encode('utf8'))
-
- result = mjml_to_html(StringIO(mjml))
- html = result.html
-
- assert ('äöüß' in html)
+@pytest.fixture
+def fs():
+ _fs = FakeFS.set_up()
+ yield _fs
+ _fs.tear_down()
+
+
+def test_can_properly_handle_include_umlauts(fs):
+ included_mjml = (
+ ''
+ ' '
+ ' äöüß'
+ ' '
+ ''
+ )
+ mjml = (
+ ''
+ ' '
+ ' foo bar'
+ ' '
+ ' '
+ ''
+ )
+ fs.create_file('footer.mjml', contents=included_mjml.encode('utf8'))
+
+ result = mjml_to_html(StringIO(mjml))
+ html = result.html
+
+ assert ('äöüß' in html)
diff --git a/tests/missing_functionality_test.py b/tests/missing_functionality_test.py
index d7f9aad..e5b11cb 100644
--- a/tests/missing_functionality_test.py
+++ b/tests/missing_functionality_test.py
@@ -1,8 +1,7 @@
from pathlib import Path
-from unittest import TestCase, expectedFailure
-from ddt import data as ddt_data, ddt as DataDrivenTestCase
+import pytest
from htmlcompare import assert_same_html
from mjml import mjml_to_html
@@ -10,41 +9,18 @@
TESTDATA_DIR = Path(__file__).parent / 'missing_functionality'
-def patch_nose1(func):
- def _wrapper(test, *args, **kwargs):
- _patch_nose1_result(test)
- return func(test, *args, **kwargs)
- return _wrapper
-
-
-@DataDrivenTestCase
-class MissingFeaturesTest(TestCase):
- @ddt_data(
- )
- @expectedFailure
- @patch_nose1
- def test_ensure_same_html(self, test_id):
- mjml_filename = f'{test_id}.mjml'
- html_filename = f'{test_id}-expected.html'
- with (TESTDATA_DIR / html_filename).open('rb') as html_fp:
- expected_html = html_fp.read()
-
- with (TESTDATA_DIR / mjml_filename).open('rb') as mjml_fp:
- result = mjml_to_html(mjml_fp)
-
- assert not result.errors
- actual_html = result.html
- assert_same_html(expected_html, actual_html, verbose=True)
-
-
-def _patch_nose1_result(test):
- # nose's TextTestResult does not support "expected failures" but I still
- # like that test runner. Just treat an expected failure like a skipped test.
- result = test._outcome.result
- if not hasattr(result, 'addExpectedFailure'):
- result.addExpectedFailure = result.addSkip
- if not hasattr(result, 'addUnexpectedSuccess'):
- def _addUnexpectedSuccess(test):
- error = (AssertionError, AssertionError('unexpected success'), None)
- return result.addFailure(test, error)
- result.addUnexpectedSuccess = _addUnexpectedSuccess
+# currently there are no tests which are expected to fail
+@pytest.mark.parametrize('test_id', [])
+@pytest.mark.xfail
+def test_missing_functionality(test_id):
+ mjml_filename = f'{test_id}.mjml'
+ html_filename = f'{test_id}-expected.html'
+ with (TESTDATA_DIR / html_filename).open('rb') as html_fp:
+ expected_html = html_fp.read()
+
+ with (TESTDATA_DIR / mjml_filename).open('rb') as mjml_fp:
+ result = mjml_to_html(mjml_fp)
+
+ assert not result.errors
+ actual_html = result.html
+ assert_same_html(expected_html, actual_html, verbose=True)
diff --git a/tests/mj_button_mailto_link_test.py b/tests/mj_button_mailto_link_test.py
index a916e22..5841118 100644
--- a/tests/mj_button_mailto_link_test.py
+++ b/tests/mj_button_mailto_link_test.py
@@ -1,35 +1,33 @@
import re
from io import StringIO
-from unittest import TestCase
import lxml.html
from mjml import mjml_to_html
-class MjButtonMailtoLinkTest(TestCase):
- def test_no_target_for_mailto_links(self):
- mjml = (
- ''
- ' '
- ' Click me'
- ' '
- ''
- )
+def test_no_target_for_mailto_links():
+ mjml = (
+ ''
+ ' '
+ ' Click me'
+ ' '
+ ''
+ )
- result = mjml_to_html(StringIO(mjml))
- html = result.html
- mailto_match = re.search(']*>', html)
- start, end = mailto_match.span()
- match_str = html[start:end]
+ result = mjml_to_html(StringIO(mjml))
+ html = result.html
+ mailto_match = re.search(']*>', html)
+ start, end = mailto_match.span()
+ match_str = html[start:end]
- a_el = lxml.html.fragment_fromstring(match_str)
- self.assertEqual('mailto:foo@site.example', a_el.attrib['href'])
- target = a_el.attrib.get('target')
- # Thunderbird opens a blank page instead of the new message window if
- # the contains 'target="_blank"'.
- # https://bugzilla.mozilla.org/show_bug.cgi?id=1677248
- # https://bugzilla.mozilla.org/show_bug.cgi?id=1589968
- # https://bugzilla.mozilla.org/show_bug.cgi?id=421310
- assert not target, f'target="{target}"'
+ a_el = lxml.html.fragment_fromstring(match_str)
+ assert a_el.attrib['href'] == 'mailto:foo@site.example'
+ target = a_el.attrib.get('target')
+ # Thunderbird opens a blank page instead of the new message window if
+ # the contains 'target="_blank"'.
+ # https://bugzilla.mozilla.org/show_bug.cgi?id=1677248
+ # https://bugzilla.mozilla.org/show_bug.cgi?id=1589968
+ # https://bugzilla.mozilla.org/show_bug.cgi?id=421310
+ assert not target, f'target="{target}"'
diff --git a/tests/mjml2html_test.py b/tests/mjml2html_test.py
index 14c76d5..043309a 100644
--- a/tests/mjml2html_test.py
+++ b/tests/mjml2html_test.py
@@ -1,17 +1,15 @@
from io import StringIO
-from unittest import TestCase
from mjml import mjml_to_html
-class MJML2HTMLTest(TestCase):
- def test_can_handle_comments_in_mjml(self):
- mjml = (
- ''
- ' '
- ' '
- ' '
- ''
- )
- mjml_to_html(StringIO(mjml))
+def test_can_handle_comments_in_mjml():
+ mjml = (
+ ''
+ ' '
+ ' '
+ ' '
+ ''
+ )
+ mjml_to_html(StringIO(mjml))
diff --git a/tests/upstream_alignment_test.py b/tests/upstream_alignment_test.py
index bd16b73..cb64d5b 100644
--- a/tests/upstream_alignment_test.py
+++ b/tests/upstream_alignment_test.py
@@ -1,156 +1,159 @@
-import sys
from json import load as json_load
-from unittest import SkipTest, TestCase, skipIf
+import pytest
from bs4 import BeautifulSoup
-from ddt import data as ddt_data, ddt as DataDrivenTestCase
from htmlcompare import assert_same_html
from mjml import mjml_to_html
from mjml.testing_helpers import get_mjml_fp, load_expected_html
-@DataDrivenTestCase
-class UpstreamAlignmentTest(TestCase):
- @ddt_data(
- 'minimal',
- 'hello-world',
- 'html-entities',
- 'html-without-closing-tag',
- 'button',
- 'text_with_html',
- 'mj-body-with-background-color',
- 'mj-breakpoint',
- 'mj-title',
- 'mj-style',
- 'mj-accordion',
- 'mj-attributes',
- 'mj-html-attributes',
- 'mj-column-with-attributes',
- 'mj-group',
- 'mj-hero-fixed',
- 'mj-hero-fluid',
- 'mj-button-with-width',
- 'mj-text-with-tail-text',
- 'mj-table',
- 'mj-head-with-comment',
- 'mj-image-with-empty-alt-attribute',
- 'mj-image-with-href',
- 'mj-section-with-full-width',
- 'mj-section-with-css-class',
- 'mj-section-with-mj-class',
- 'mj-section-with-background-url',
- 'mj-section-with-background',
- 'mj-font',
- 'mj-font-multiple',
- 'mj-font-unused',
- 'mj-include-body',
- 'mj-preview',
- 'mj-raw',
- 'mj-raw-with-tags',
- 'mj-raw-head',
- 'mj-raw-head-with-tags',
- 'mj-social',
- 'mj-spacer',
- 'mj-wrapper',
- )
- def test_ensure_same_html(self, test_id):
- expected_html = load_expected_html(test_id)
- with get_mjml_fp(test_id) as mjml_fp:
- result = mjml_to_html(mjml_fp)
-
- assert not result.errors
- actual_html = result.html
- assert_same_html(expected_html, actual_html, verbose=True)
-
- @ddt_data('hello-world')
- def test_ensure_same_html_from_json(self, test_id):
- expected_html = load_expected_html(test_id)
- with get_mjml_fp(test_id, json=True) as mjml_json_fp:
- result = mjml_to_html(json_load(mjml_json_fp))
-
- assert not result.errors
- actual_html = result.html
- assert_same_html(expected_html, actual_html, verbose=True)
-
- def test_accepts_also_plain_strings_as_input(self):
- test_id = 'hello-world'
- expected_html = load_expected_html(test_id)
- with get_mjml_fp(test_id) as mjml_fp:
- mjml_str = mjml_fp.read().decode('utf8')
- result = mjml_to_html(mjml_str)
-
- assert not result.errors
- actual_html = result.html
- assert_same_html(expected_html, actual_html, verbose=True)
-
- @skipIf(sys.version_info < (3, 7), reason='css_inline requires >= python3.7')
- def test_can_use_css_inlining(self):
- try:
- import css_inline # noqa: F401 (unused-import)
- except ImportError:
- raise SkipTest('"css_inline" not installed')
- test_id = 'css-inlining'
- expected_html = load_expected_html(test_id)
- with get_mjml_fp(test_id) as mjml_fp:
- mjml_str = mjml_fp.read().decode('utf8')
- result = mjml_to_html(mjml_str)
-
- assert not result.errors
- assert_same_html(expected_html, result.html, verbose=True)
-
- # The dynamically generated menu key prevents us from just using
- # test_ensure_same_html to test mj-navbar
- def test_mj_navbar(self):
- test_id = 'mj-navbar'
- expected_html = load_expected_html(test_id)
- with get_mjml_fp(test_id) as mjml_fp:
- result = mjml_to_html(mjml_fp)
-
- assert not result.errors
- expected_soup = BeautifulSoup(expected_html, 'html.parser')
- actual_soup = BeautifulSoup(result.html, 'html.parser')
-
- # This key is randomly generated, so we need to manually replace it.
- menuKey = actual_soup.find(attrs={'class': 'mj-menu-checkbox'})['id']
- expected_soup.find(attrs={'class': 'mj-menu-checkbox'})['id'] = menuKey
- expected_soup.find(attrs={'class': 'mj-menu-label'})['for'] = menuKey
-
- assert_same_html(str(expected_soup), str(actual_soup), verbose=True)
-
- # The dynamically generated carousel ID prevents us from just using
- # test_ensure_same_html to test mj-carousel
- def test_mj_carousel(self):
- test_id = 'mj-carousel'
- expected_html = load_expected_html(test_id)
- with get_mjml_fp(test_id) as mjml_fp:
- result = mjml_to_html(mjml_fp)
-
- assert not result.errors
- expected_soup = BeautifulSoup(expected_html, 'html.parser')
- actual_soup = BeautifulSoup(result.html, 'html.parser')
-
- # This ID is randomly generated, so we need to manually replace it.
- def _replace_random_radio_class(soup):
- _mj_cr_str = 'mj-carousel-radio'
- return soup.find(attrs={'class': _mj_cr_str})['name'].replace(f'{_mj_cr_str}-', '')
- expected_carousel_id = _replace_random_radio_class(expected_soup)
- actual_carousel_id = _replace_random_radio_class(actual_soup)
- actual_html = str(actual_soup).replace(actual_carousel_id, expected_carousel_id)
- assert_same_html(str(expected_soup), actual_html, verbose=True)
-
- # htmlcompare is currently unable to detect these kind of
- # whitespace differences.
- def test_keep_whitespace_before_tag(self):
- test_id = 'missing-whitespace-before-tag'
- expected_html = load_expected_html(test_id)
- with get_mjml_fp(test_id) as mjml_fp:
- result = mjml_to_html(mjml_fp)
-
- assert not result.errors
- expected_text = BeautifulSoup(expected_html, 'html.parser').body.get_text().strip()
- body_actual = BeautifulSoup(result.html, 'html.parser').body
- actual_text = body_actual.get_text().strip()
- assert (expected_text == actual_text)
- actual_html = (body_actual.select('.mj-column-per-100 div')[0]).renderContents()
- assert (b'foo bar.' == actual_html)
+TEST_IDS = (
+ 'minimal',
+ 'hello-world',
+ 'html-entities',
+ 'html-without-closing-tag',
+ 'button',
+ 'text_with_html',
+ 'mj-body-with-background-color',
+ 'mj-breakpoint',
+ 'mj-title',
+ 'mj-style',
+ 'mj-accordion',
+ 'mj-attributes',
+ 'mj-html-attributes',
+ 'mj-column-with-attributes',
+ 'mj-group',
+ 'mj-hero-fixed',
+ 'mj-hero-fluid',
+ 'mj-button-with-width',
+ 'mj-text-with-tail-text',
+ 'mj-table',
+ 'mj-head-with-comment',
+ 'mj-image-with-empty-alt-attribute',
+ 'mj-image-with-href',
+ 'mj-section-with-full-width',
+ 'mj-section-with-css-class',
+ 'mj-section-with-mj-class',
+ 'mj-section-with-background-url',
+ 'mj-section-with-background',
+ 'mj-font',
+ 'mj-font-multiple',
+ 'mj-font-unused',
+ 'mj-include-body',
+ 'mj-preview',
+ 'mj-raw',
+ 'mj-raw-with-tags',
+ 'mj-raw-head',
+ 'mj-raw-head-with-tags',
+ 'mj-social',
+ 'mj-spacer',
+ 'mj-wrapper',
+)
+
+@pytest.mark.parametrize('test_id', TEST_IDS)
+def test_ensure_same_html_as_upstream(test_id):
+ expected_html = load_expected_html(test_id)
+ with get_mjml_fp(test_id) as mjml_fp:
+ result = mjml_to_html(mjml_fp)
+
+ assert not result.errors
+ actual_html = result.html
+ assert_same_html(expected_html, actual_html, verbose=True)
+
+
+def test_ensure_same_html_from_json():
+ test_id = 'hello-world'
+ expected_html = load_expected_html(test_id)
+ with get_mjml_fp(test_id, json=True) as mjml_json_fp:
+ result = mjml_to_html(json_load(mjml_json_fp))
+
+ assert not result.errors
+ actual_html = result.html
+ assert_same_html(expected_html, actual_html, verbose=True)
+
+
+def test_accepts_also_plain_strings_as_input():
+ test_id = 'hello-world'
+ expected_html = load_expected_html(test_id)
+ with get_mjml_fp(test_id) as mjml_fp:
+ mjml_str = mjml_fp.read().decode('utf8')
+ result = mjml_to_html(mjml_str)
+
+ assert not result.errors
+ actual_html = result.html
+ assert_same_html(expected_html, actual_html, verbose=True)
+
+
+def test_can_use_css_inlining():
+ try:
+ import css_inline # noqa: F401 (unused-import)
+ except ImportError:
+ pytest.skip('"css_inline" not installed')
+ test_id = 'css-inlining'
+ expected_html = load_expected_html(test_id)
+ with get_mjml_fp(test_id) as mjml_fp:
+ mjml_str = mjml_fp.read().decode('utf8')
+ result = mjml_to_html(mjml_str)
+
+ assert not result.errors
+ assert_same_html(expected_html, result.html, verbose=True)
+
+
+# The dynamically generated menu key prevents us from just using
+# test_ensure_same_html to test mj-navbar
+def test_mj_navbar():
+ test_id = 'mj-navbar'
+ expected_html = load_expected_html(test_id)
+ with get_mjml_fp(test_id) as mjml_fp:
+ result = mjml_to_html(mjml_fp)
+
+ assert not result.errors
+ expected_soup = BeautifulSoup(expected_html, 'html.parser')
+ actual_soup = BeautifulSoup(result.html, 'html.parser')
+
+ # This key is randomly generated, so we need to manually replace it.
+ menuKey = actual_soup.find(attrs={'class': 'mj-menu-checkbox'})['id']
+ expected_soup.find(attrs={'class': 'mj-menu-checkbox'})['id'] = menuKey
+ expected_soup.find(attrs={'class': 'mj-menu-label'})['for'] = menuKey
+
+ assert_same_html(str(expected_soup), str(actual_soup), verbose=True)
+
+
+# The dynamically generated carousel ID prevents us from just using
+# test_ensure_same_html to test mj-carousel
+def test_mj_carousel():
+ test_id = 'mj-carousel'
+ expected_html = load_expected_html(test_id)
+ with get_mjml_fp(test_id) as mjml_fp:
+ result = mjml_to_html(mjml_fp)
+
+ assert not result.errors
+ expected_soup = BeautifulSoup(expected_html, 'html.parser')
+ actual_soup = BeautifulSoup(result.html, 'html.parser')
+
+ # This ID is randomly generated, so we need to manually replace it.
+ def _replace_random_radio_class(soup):
+ _mj_cr_str = 'mj-carousel-radio'
+ return soup.find(attrs={'class': _mj_cr_str})['name'].replace(f'{_mj_cr_str}-', '')
+ expected_carousel_id = _replace_random_radio_class(expected_soup)
+ actual_carousel_id = _replace_random_radio_class(actual_soup)
+ actual_html = str(actual_soup).replace(actual_carousel_id, expected_carousel_id)
+ assert_same_html(str(expected_soup), actual_html, verbose=True)
+
+
+# htmlcompare is currently unable to detect these kind of
+# whitespace differences.
+def test_keep_whitespace_before_tag():
+ test_id = 'missing-whitespace-before-tag'
+ expected_html = load_expected_html(test_id)
+ with get_mjml_fp(test_id) as mjml_fp:
+ result = mjml_to_html(mjml_fp)
+
+ assert not result.errors
+ expected_text = BeautifulSoup(expected_html, 'html.parser').body.get_text().strip()
+ body_actual = BeautifulSoup(result.html, 'html.parser').body
+ actual_text = body_actual.get_text().strip()
+ assert (expected_text == actual_text)
+ actual_html = (body_actual.select('.mj-column-per-100 div')[0]).renderContents()
+ assert (b'foo bar.' == actual_html)