Repository: qutebrowser/qutebrowser

Issue:
Logo
qutebrowser still needs a logo!

Some random ideas:
- `qutebrowser` in some "cute" (fur?) font
- A `q` which is composed of a globe (because browsers need a globe) and a snake "hanging" around it. Ideally with either the snake or the globe being cute. :grin:



Discussion:
I'm no artist, but here's my (and el jefe's) thoughts on the matter (for anyone wondering).

```
11:10 < iggy> the q would probably be the easiest to get to scale down well, but is probably least likely to draw people in if it is used elsewhere
11:10 < iggy> (i.e. if you start using it on the website, etc.)
11:11 < The-Compiler> yeah, but I like the idea more :)
11:12 < The-Compiler> and it'd still be possible to make a whole matching "qutebrowser" logo with that q I guess
```

So I guess the q idea is the winner here.

`q` made out of a globe with the tail being a snake?

Closed in 987ca1dd4a6347200b796c1072b8999a1914bc16 thanks to WOFall.


Resolving patch:
diff --git a/qutebrowser/misc/split.py b/qutebrowser/misc/split.py
--- a/qutebrowser/misc/split.py
+++ b/qutebrowser/misc/split.py
@@ -127,7 +127,7 @@ def split(s, keep=False):
     """Split a string via ShellLexer.
 
     Args:
-        keep: Whether to keep are special chars in the split output.
+        keep: Whether to keep special chars in the split output.
     """
     lexer = ShellLexer(s)
     lexer.keep = keep


Test patch:
diff --git a/tests/commands/test_runners.py b/tests/commands/test_runners.py
new file mode 100644
--- /dev/null
+++ b/tests/commands/test_runners.py
@@ -0,0 +1,44 @@
+# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
+
+# Copyright 2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
+#
+# This file is part of qutebrowser.
+#
+# qutebrowser is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# qutebrowser is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with qutebrowser.  If not, see <http://www.gnu.org/licenses/>.
+
+"""Tests for qutebrowser.commands.runners."""
+
+import pytest
+
+from qutebrowser.commands import runners, cmdexc
+
+
+class TestCommandRunner:
+
+    """Tests for CommandRunner."""
+
+    def test_parse_all(self, cmdline_test):
+        """Test parsing of commands.
+
+        See https://github.com/The-Compiler/qutebrowser/issues/615
+
+        Args:
+            cmdline_test: A pytest fixture which provides testcases.
+        """
+        cr = runners.CommandRunner(0)
+        if cmdline_test.valid:
+            list(cr.parse_all(cmdline_test.cmd, aliases=False))
+        else:
+            with pytest.raises(cmdexc.NoSuchCommandError):
+                list(cr.parse_all(cmdline_test.cmd, aliases=False))
diff --git a/tests/config/test_config.py b/tests/config/test_config.py
--- a/tests/config/test_config.py
+++ b/tests/config/test_config.py
@@ -31,7 +31,9 @@
 from PyQt5.QtGui import QColor
 import pytest
 
-from qutebrowser.config import config, configexc
+from qutebrowser.config import config, configexc, configdata
+from qutebrowser.config.parsers import keyconf
+from qutebrowser.commands import runners
 from qutebrowser.utils import objreg, standarddir
 
 
@@ -155,6 +157,27 @@ def test_invalid_option_relaxed(self):
             self.cfg.get('general', 'bar')  # pylint: disable=bad-config-call
 
 
+class TestKeyConfigParser:
+
+    """Test config.parsers.keyconf.KeyConfigParser."""
+
+    def test_cmd_binding(self, cmdline_test):
+        """Test various command bindings.
+
+        See https://github.com/The-Compiler/qutebrowser/issues/615
+
+        Args:
+            cmdline_test: A pytest fixture which provides testcases.
+        """
+        kcp = keyconf.KeyConfigParser(None, None)
+        kcp._cur_section = 'normal'
+        if cmdline_test.valid:
+            kcp._read_command(cmdline_test.cmd)
+        else:
+            with pytest.raises(keyconf.KeyConfigError):
+                kcp._read_command(cmdline_test.cmd)
+
+
 class TestDefaultConfig:
 
     """Test validating of the default config."""
@@ -164,6 +187,16 @@ def test_default_config(self):
         conf = config.ConfigManager(None, None)
         conf._validate_all()
 
+    def test_default_key_config(self):
+        """Test validating of the default key config."""
+        # We import qutebrowser.app so the cmdutils.register decorators run.
+        import qutebrowser.app  # pylint: disable=unused-variable
+        conf = keyconf.KeyConfigParser(None, None)
+        runner = runners.CommandRunner(win_id=0)
+        for sectname in configdata.KEY_DATA:
+            for cmd in conf.get_bindings_for(sectname).values():
+                runner.parse(cmd, aliases=False)
+
 
 class TestConfigInit:
 
diff --git a/tests/conftest.py b/tests/conftest.py
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -19,6 +19,9 @@
 
 """The qutebrowser test suite contest file."""
 
+import collections
+import itertools
+
 import pytest
 
 
@@ -49,15 +52,23 @@ def unicode_encode_err():
                               'fake exception')  # reason
 
 
+@pytest.fixture(scope='session')
+def qnam():
+    """Session-wide QNetworkAccessManager."""
+    from PyQt5.QtNetwork import QNetworkAccessManager
+    nam = QNetworkAccessManager()
+    nam.setNetworkAccessible(QNetworkAccessManager.NotAccessible)
+    return nam
+
+
 @pytest.fixture
-def webpage():
+def webpage(qnam):
     """Get a new QWebPage object."""
     from PyQt5.QtWebKitWidgets import QWebPage
-    from PyQt5.QtNetwork import QNetworkAccessManager
 
     page = QWebPage()
-    nam = page.networkAccessManager()
-    nam.setNetworkAccessible(QNetworkAccessManager.NotAccessible)
+    page.networkAccessManager().deleteLater()
+    page.setNetworkAccessManager(qnam)
     return page
 
 
@@ -76,3 +87,63 @@ def fake_keyevent(key, modifiers=0, text=''):
         return evtmock
 
     return fake_keyevent
+
+
+def pytest_collection_modifyitems(items):
+    """Automatically add a 'gui' marker to all gui-related tests.
+
+    pytest hook called after collection has been performed, adds a marker
+    named "gui" which can be used to filter gui tests from the command line.
+    For example:
+
+        py.test -m "not gui"  # run all tests except gui tests
+        py.test -m "gui"  # run only gui tests
+
+    Args:
+        items: list of _pytest.main.Node items, where each item represents
+               a python test that will be executed.
+
+    Reference:
+        http://pytest.org/latest/plugins.html
+    """
+    for item in items:
+        if 'qtbot' in getattr(item, 'fixturenames', ()):
+            item.add_marker('gui')
+
+
+def _generate_cmdline_tests():
+    """Generate testcases for test_split_binding."""
+    # pylint: disable=invalid-name
+    TestCase = collections.namedtuple('TestCase', 'cmd, valid')
+    separators = [';;', ' ;; ', ';; ', ' ;;']
+    invalid = ['foo', '']
+    valid = ['leave-mode', 'hint all']
+    # Valid command only -> valid
+    for item in valid:
+        yield TestCase(''.join(item), True)
+    # Invalid command only -> invalid
+    for item in valid:
+        yield TestCase(''.join(item), True)
+    # Invalid command combined with invalid command -> invalid
+    for item in itertools.product(invalid, separators, invalid):
+        yield TestCase(''.join(item), False)
+    # Valid command combined with valid command -> valid
+    for item in itertools.product(valid, separators, valid):
+        yield TestCase(''.join(item), True)
+    # Valid command combined with invalid command -> invalid
+    for item in itertools.product(valid, separators, invalid):
+        yield TestCase(''.join(item), False)
+    # Invalid command combined with valid command -> invalid
+    for item in itertools.product(invalid, separators, valid):
+        yield TestCase(''.join(item), False)
+    # Command with no_cmd_split combined with an "invalid" command -> valid
+    for item in itertools.product(['bind x open'], separators, invalid):
+        yield TestCase(''.join(item), True)
+
+
+@pytest.fixture(params=_generate_cmdline_tests())
+def cmdline_test(request):
+    """Fixture which generates tests for things validating commandlines."""
+    # Import qutebrowser.app so all cmdutils.register decorators get run.
+    import qutebrowser.app  # pylint: disable=unused-variable
+    return request.param
diff --git a/tests/keyinput/test_basekeyparser.py b/tests/keyinput/test_basekeyparser.py
--- a/tests/keyinput/test_basekeyparser.py
+++ b/tests/keyinput/test_basekeyparser.py
@@ -63,44 +63,27 @@ class TestSplitCount:
 
     """Test the _split_count method.
 
-    Attributes:
-        kp: The BaseKeyParser we're testing.
+    Class Attributes:
+        TESTS: list of parameters for the tests, as tuples of
+        (input_key, supports_count, expected)
     """
 
-    @pytest.fixture(autouse=True)
-    def setup(self):
-        self.kp = basekeyparser.BaseKeyParser(0, supports_count=True)
-
-    def test_onlycount(self):
+    TESTS = [
+        # (input_key, supports_count, expected)
+        ('10', True, (10, '')),
+        ('10foo', True, (10, 'foo')),
+        ('-1foo', True, (None, '-1foo')),
+        ('10e4foo', True, (10, 'e4foo')),
+        ('foo', True, (None, 'foo')),
+        ('10foo', False, (None, '10foo')),
+    ]
+
+    @pytest.mark.parametrize('input_key, supports_count, expected', TESTS)
+    def test_splitcount(self, input_key, supports_count, expected):
         """Test split_count with only a count."""
-        self.kp._keystring = '10'
-        assert self.kp._split_count() == (10, '')
-
-    def test_normalcount(self):
-        """Test split_count with count and text."""
-        self.kp._keystring = '10foo'
-        assert self.kp._split_count() == (10, 'foo')
-
-    def test_minuscount(self):
-        """Test split_count with a negative count."""
-        self.kp._keystring = '-1foo'
-        assert self.kp._split_count() == (None, '-1foo')
-
-    def test_expcount(self):
-        """Test split_count with an exponential count."""
-        self.kp._keystring = '10e4foo'
-        assert self.kp._split_count() == (10, 'e4foo')
-
-    def test_nocount(self):
-        """Test split_count with only a command."""
-        self.kp._keystring = 'foo'
-        assert self.kp._split_count() == (None, 'foo')
-
-    def test_nosupport(self):
-        """Test split_count with a count when counts aren't supported."""
-        self.kp._supports_count = False
-        self.kp._keystring = '10foo'
-        assert self.kp._split_count() == (None, '10foo')
+        kp = basekeyparser.BaseKeyParser(0, supports_count=supports_count)
+        kp._keystring = input_key
+        assert kp._split_count() == expected
 
 
 @pytest.mark.usefixtures('fake_keyconfig', 'mock_timer')
diff --git a/tests/misc/test_miscwidgets.py b/tests/misc/test_miscwidgets.py
new file mode 100644
--- /dev/null
+++ b/tests/misc/test_miscwidgets.py
@@ -0,0 +1,90 @@
+# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
+
+# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
+#
+# This file is part of qutebrowser.
+#
+# qutebrowser is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# qutebrowser is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with qutebrowser.  If not, see <http://www.gnu.org/licenses/>.
+
+"""Test widgets in miscwidgets module."""
+
+from unittest import mock
+from PyQt5.QtCore import Qt
+from PyQt5.QtWidgets import QApplication
+import pytest
+
+from qutebrowser.misc.miscwidgets import CommandLineEdit
+
+
+class TestCommandLineEdit:
+
+    """Tests for CommandLineEdit widget."""
+
+    @pytest.yield_fixture
+    def cmd_edit(self, qtbot):
+        """Fixture to initialize a CommandLineEdit."""
+        cmd_edit = CommandLineEdit(None)
+        cmd_edit.set_prompt(':')
+        qtbot.add_widget(cmd_edit)
+        assert cmd_edit.text() == ''
+        yield cmd_edit
+
+    @pytest.fixture
+    def mock_clipboard(self, mocker):
+        """Fixture to mock QApplication.clipboard.
+
+        Return:
+            The mocked QClipboard object.
+        """
+        mocker.patch.object(QApplication, 'clipboard')
+        clipboard = mock.MagicMock()
+        clipboard.supportsSelection.return_value = True
+        QApplication.clipboard.return_value = clipboard
+        return clipboard
+
+    def test_position(self, qtbot, cmd_edit):
+        """Test cursor position based on the prompt."""
+        qtbot.keyClicks(cmd_edit, ':hello')
+        assert cmd_edit.text() == ':hello'
+        assert cmd_edit.cursorPosition() == len(':hello')
+
+        cmd_edit.home(mark=True)
+        assert cmd_edit.cursorPosition() == len(':hello')
+        qtbot.keyClick(cmd_edit, Qt.Key_Delete)
+        assert cmd_edit.text() == ':'
+        qtbot.keyClick(cmd_edit, Qt.Key_Backspace)
+        assert cmd_edit.text() == ':'
+
+        qtbot.keyClicks(cmd_edit, 'hey again')
+        assert cmd_edit.text() == ':hey again'
+
+    def test_invalid_prompt(self, qtbot, cmd_edit):
+        """Test preventing of an invalid prompt being entered."""
+        qtbot.keyClicks(cmd_edit, '$hello')
+        assert cmd_edit.text() == ''
+
+    def test_clipboard_paste(self, qtbot, cmd_edit, mock_clipboard):
+        """Test pasting commands from clipboard."""
+        mock_clipboard.text.return_value = ':command'
+        qtbot.keyClick(cmd_edit, Qt.Key_Insert, Qt.ShiftModifier)
+        assert cmd_edit.text() == ':command'
+
+        mock_clipboard.text.return_value = ' param1'
+        qtbot.keyClick(cmd_edit, Qt.Key_Insert, Qt.ShiftModifier)
+        assert cmd_edit.text() == ':command param1'
+
+        cmd_edit.clear()
+        mock_clipboard.text.return_value = '$ command'
+        qtbot.keyClick(cmd_edit, Qt.Key_Insert, Qt.ShiftModifier)
+        assert cmd_edit.text() == ':command param1'
diff --git a/tests/misc/test_split.py b/tests/misc/test_split.py
--- a/tests/misc/test_split.py
+++ b/tests/misc/test_split.py
@@ -18,8 +18,9 @@
 # along with qutebrowser.  If not, see <http://www.gnu.org/licenses/>.
 
 """Tests for qutebrowser.misc.split."""
+import collections
 
-import unittest
+import pytest
 
 from qutebrowser.misc import split
 
@@ -29,7 +30,7 @@
 
 # Format: input/split|output|without|keep/split|output|with|keep/
 
-test_data = r"""
+test_data_str = r"""
 one two/one|two/one| two/
 one "two three" four/one|two three|four/one| "two three"| four/
 one 'two three' four/one|two three|four/one| 'two three'| four/
@@ -104,36 +105,56 @@
 """
 
 
-class SplitTests(unittest.TestCase):
+def _parse_split_test_data_str():
+    """
+    Parse the test data set into a namedtuple to use in tests.
+
+    Returns:
+        A list of namedtuples with str attributes: input, keep, no_keep
+    """
+    tuple_class = collections.namedtuple('TestCase', 'input, keep, no_keep')
+
+    result = []
+    for line in test_data_str.splitlines():
+        if not line:
+            continue
+        data = line.split('/')
+        item = tuple_class(input=data[0], keep=data[1].split('|'),
+                           no_keep=data[2].split('|'))
+        result.append(item)
+    return result
+
+
+class TestSplit:
 
     """Test split."""
 
-    def test_split(self):
+    @pytest.fixture(params=_parse_split_test_data_str())
+    def split_test_case(self, request):
+        """Fixture to automatically parametrize all depending tests.
+
+        It will use the test data from test_data_str, parsed using
+        _parse_split_test_data_str().
+        """
+        return request.param
+
+    def test_split(self, split_test_case):
         """Test splitting."""
-        for case in test_data.strip().splitlines():
-            cmd, out = case.split('/')[:-2]
-            with self.subTest(cmd=cmd):
-                items = split.split(cmd)
-                self.assertEqual(items, out.split('|'))
+        items = split.split(split_test_case.input)
+        assert items == split_test_case.keep
 
-    def test_split_keep_original(self):
+    def test_split_keep_original(self, split_test_case):
         """Test if splitting with keep=True yields the original string."""
-        for case in test_data.strip().splitlines():
-            cmd = case.split('/')[0]
-            with self.subTest(cmd=cmd):
-                items = split.split(cmd, keep=True)
-                self.assertEqual(''.join(items), cmd)
+        items = split.split(split_test_case.input, keep=True)
+        assert ''.join(items) == split_test_case.input
 
-    def test_split_keep(self):
+    def test_split_keep(self, split_test_case):
         """Test splitting with keep=True."""
-        for case in test_data.strip().splitlines():
-            cmd, _mid, out = case.split('/')[:-1]
-            with self.subTest(cmd=cmd):
-                items = split.split(cmd, keep=True)
-                self.assertEqual(items, out.split('|'))
+        items = split.split(split_test_case.input, keep=True)
+        assert items == split_test_case.no_keep
 
 
-class SimpleSplitTests(unittest.TestCase):
+class TestSimpleSplit:
 
     """Test simple_split."""
 
@@ -145,27 +166,20 @@ class SimpleSplitTests(unittest.TestCase):
         'foo\nbar': ['foo', '\nbar'],
     }
 
-    def test_str_split(self):
+    @pytest.mark.parametrize('test', TESTS)
+    def test_str_split(self, test):
         """Test if the behavior matches str.split."""
-        for test in self.TESTS:
-            with self.subTest(string=test):
-                self.assertEqual(split.simple_split(test),
-                                 test.rstrip().split())
-
-    def test_str_split_maxsplit_1(self):
-        """Test if the behavior matches str.split with maxsplit=1."""
-        string = "foo bar baz"
-        self.assertEqual(split.simple_split(string, maxsplit=1),
-                         string.rstrip().split(maxsplit=1))
-
-    def test_str_split_maxsplit_0(self):
-        """Test if the behavior matches str.split with maxsplit=0."""
-        string = "  foo bar baz  "
-        self.assertEqual(split.simple_split(string, maxsplit=0),
-                         string.rstrip().split(maxsplit=0))
-
-    def test_split_keep(self):
+        assert split.simple_split(test) == test.rstrip().split()
+
+    @pytest.mark.parametrize('s, maxsplit',
+                             [("foo bar baz", 1), ("  foo bar baz  ", 0)])
+    def test_str_split_maxsplit(self, s, maxsplit):
+        """Test if the behavior matches str.split with given maxsplit."""
+        actual = split.simple_split(s, maxsplit=maxsplit)
+        expected = s.rstrip().split(maxsplit=maxsplit)
+        assert actual == expected
+
+    @pytest.mark.parametrize('test, expected', TESTS.items())
+    def test_split_keep(self, test, expected):
         """Test splitting with keep=True."""
-        for test, expected in self.TESTS.items():
-            with self.subTest(string=test):
-                self.assertEqual(split.simple_split(test, keep=True), expected)
+        assert split.simple_split(test, keep=True) == expected
diff --git a/tests/utils/overflow_test_cases.py b/tests/utils/overflow_test_cases.py
new file mode 100644
--- /dev/null
+++ b/tests/utils/overflow_test_cases.py
@@ -0,0 +1,70 @@
+# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
+
+# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
+#
+# This file is part of qutebrowser.
+#
+# qutebrowser is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# qutebrowser is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with qutebrowser.  If not, see <http://www.gnu.org/licenses/>.
+
+"""
+Provides test data for overflow checking.
+
+Module attributes:
+    INT32_MIN: Minimum valid value for a signed int32.
+    INT32_MAX: Maximum valid value for a signed int32.
+    INT64_MIN: Minimum valid value for a signed int64.
+    INT64_MAX: Maximum valid value for a signed int64.
+    GOOD_VALUES: A dict of types mapped to a list of good values.
+    BAD_VALUES: A dict of types mapped to a list of bad values.
+"""
+
+INT32_MIN = -(2 ** 31)
+INT32_MAX = 2 ** 31 - 1
+INT64_MIN = -(2 ** 63)
+INT64_MAX = 2 ** 63 - 1
+
+GOOD_VALUES = {
+    'int': [-1, 0, 1, 23.42, INT32_MIN, INT32_MAX],
+    'int64': [-1, 0, 1, 23.42, INT64_MIN, INT64_MAX],
+}
+
+BAD_VALUES = {
+    'int': [(INT32_MIN - 1, INT32_MIN),
+            (INT32_MAX + 1, INT32_MAX),
+            (float(INT32_MAX + 1), INT32_MAX)],
+    'int64': [(INT64_MIN - 1, INT64_MIN),
+              (INT64_MAX + 1, INT64_MAX),
+              (float(INT64_MAX + 1), INT64_MAX)],
+}
+
+
+def iter_good_values():
+    """Yield "good" (C data type, value) tuples.
+
+    Those should pass overflow checking.
+    """
+    for ctype, values in GOOD_VALUES.items():
+        for value in values:
+            yield ctype, value
+
+
+def iter_bad_values():
+    """Yield pairs of "bad" (C type, value, repl) tuples.
+
+    Theose should not pass overflow checking. The third value is the value they
+    should be replaced with if overflow checking should not be fatal.
+    """
+    for ctype, values in BAD_VALUES.items():
+        for value, repl in values:
+            yield ctype, value, repl
diff --git a/tests/utils/test_jinja.py b/tests/utils/test_jinja.py
--- a/tests/utils/test_jinja.py
+++ b/tests/utils/test_jinja.py
@@ -20,47 +20,42 @@
 """Tests for qutebrowser.utils.jinja."""
 
 import os.path
-import unittest
-import unittest.mock
-
-from qutebrowser.utils import jinja
 
+import pytest
 
-def _read_file(path):
-    """Mocked utils.read_file."""
-    if path == os.path.join('html', 'test.html'):
-        return """Hello {{var}}"""
-    else:
-        raise ValueError("Invalid path {}!".format(path))
+from qutebrowser.utils import jinja
 
 
-@unittest.mock.patch('qutebrowser.utils.jinja.utils.read_file')
-class JinjaTests(unittest.TestCase):
+@pytest.fixture(autouse=True)
+def patch_read_file(monkeypatch):
+    """pytest fixture to patch utils.read_file."""
+    def _read_file(path):
+        """A read_file which returns a simple template if the path is right."""
+        if path == os.path.join('html', 'test.html'):
+            return """Hello {{var}}"""
+        else:
+            raise ValueError("Invalid path {}!".format(path))
 
-    """Tests for getting template via jinja."""
+    monkeypatch.setattr('qutebrowser.utils.jinja.utils.read_file', _read_file)
 
-    def test_simple_template(self, readfile_mock):
-        """Test with a simple template."""
-        readfile_mock.side_effect = _read_file
-        template = jinja.env.get_template('test.html')
-        # https://bitbucket.org/logilab/pylint/issue/490/
-        data = template.render(var='World')  # pylint: disable=no-member
-        self.assertEqual(data, "Hello World")
 
-    def test_utf8(self, readfile_mock):
-        """Test rendering with an UTF8 template.
+def test_simple_template():
+    """Test with a simple template."""
+    template = jinja.env.get_template('test.html')
+    # https://bitbucket.org/logilab/pylint/issue/490/
+    data = template.render(var='World')  # pylint: disable=no-member
+    assert data == "Hello World"
 
-        This was an attempt to get a failing test case for #127 but it seems
-        the issue is elsewhere.
 
-        https://github.com/The-Compiler/qutebrowser/issues/127
-        """
-        readfile_mock.side_effect = _read_file
-        template = jinja.env.get_template('test.html')
-        # https://bitbucket.org/logilab/pylint/issue/490/
-        data = template.render(var='\u2603')  # pylint: disable=no-member
-        self.assertEqual(data, "Hello \u2603")
+def test_utf8():
+    """Test rendering with an UTF8 template.
 
+    This was an attempt to get a failing test case for #127 but it seems
+    the issue is elsewhere.
 
-if __name__ == '__main__':
-    unittest.main()
+    https://github.com/The-Compiler/qutebrowser/issues/127
+    """
+    template = jinja.env.get_template('test.html')
+    # https://bitbucket.org/logilab/pylint/issue/490/
+    data = template.render(var='\u2603')  # pylint: disable=no-member
+    assert data == "Hello \u2603"
diff --git a/tests/utils/test_qtutils.py b/tests/utils/test_qtutils.py
--- a/tests/utils/test_qtutils.py
+++ b/tests/utils/test_qtutils.py
@@ -20,107 +20,74 @@
 """Tests for qutebrowser.utils.qtutils."""
 
 import sys
-import unittest
+
+import pytest
 
 from qutebrowser import qutebrowser
 from qutebrowser.utils import qtutils
+import overflow_test_cases
 
 
-class CheckOverflowTests(unittest.TestCase):
-
-    """Test check_overflow.
-
-    Class attributes:
-        INT32_MIN: Minimum valid value for a signed int32.
-        INT32_MAX: Maximum valid value for a signed int32.
-        INT64_MIN: Minimum valid value for a signed int64.
-        INT64_MAX: Maximum valid value for a signed int64.
-        GOOD_VALUES: A dict of types mapped to a list of good values.
-        BAD_VALUES: A dict of types mapped to a list of bad values.
-    """
-
-    INT32_MIN = -(2 ** 31)
-    INT32_MAX = 2 ** 31 - 1
-    INT64_MIN = -(2 ** 63)
-    INT64_MAX = 2 ** 63 - 1
-
-    GOOD_VALUES = {
-        'int': [-1, 0, 1, 23.42, INT32_MIN, INT32_MAX],
-        'int64': [-1, 0, 1, 23.42, INT64_MIN, INT64_MAX],
-    }
+class TestCheckOverflow:
 
-    BAD_VALUES = {
-        'int': [(INT32_MIN - 1, INT32_MIN),
-                (INT32_MAX + 1, INT32_MAX),
-                (float(INT32_MAX + 1), INT32_MAX)],
-        'int64': [(INT64_MIN - 1, INT64_MIN),
-                  (INT64_MAX + 1, INT64_MAX),
-                  (float(INT64_MAX + 1), INT64_MAX)],
-    }
+    """Test check_overflow."""
 
-    def test_good_values(self):
+    @pytest.mark.parametrize('ctype, val',
+                             overflow_test_cases.iter_good_values())
+    def test_good_values(self, ctype, val):
         """Test values which are inside bounds."""
-        for ctype, vals in self.GOOD_VALUES.items():
-            for val in vals:
-                with self.subTest(ctype=ctype, val=val):
-                    qtutils.check_overflow(val, ctype)
+        qtutils.check_overflow(val, ctype)
 
-    def test_bad_values_fatal(self):
+    @pytest.mark.parametrize('ctype, val',
+                             [(ctype, val) for (ctype, val, _) in
+                              overflow_test_cases.iter_bad_values()])
+    def test_bad_values_fatal(self, ctype, val):
         """Test values which are outside bounds with fatal=True."""
-        for ctype, vals in self.BAD_VALUES.items():
-            for (val, _) in vals:
-                with self.subTest(ctype=ctype, val=val):
-                    with self.assertRaises(OverflowError):
-                        qtutils.check_overflow(val, ctype)
+        with pytest.raises(OverflowError):
+            qtutils.check_overflow(val, ctype)
 
-    def test_bad_values_nonfatal(self):
+    @pytest.mark.parametrize('ctype, val, repl',
+                             overflow_test_cases.iter_bad_values())
+    def test_bad_values_nonfatal(self, ctype, val, repl):
         """Test values which are outside bounds with fatal=False."""
-        for ctype, vals in self.BAD_VALUES.items():
-            for (val, replacement) in vals:
-                with self.subTest(ctype=ctype, val=val):
-                    newval = qtutils.check_overflow(val, ctype, fatal=False)
-                    self.assertEqual(newval, replacement)
+        newval = qtutils.check_overflow(val, ctype, fatal=False)
+        assert newval == repl
 
 
-def argparser_exit(status=0, message=None):  # pylint: disable=unused-argument
-    """Function to monkey-patch .exit() of the argparser so it doesn't exit."""
-    raise Exception
-
-
-class GetQtArgsTests(unittest.TestCase):
+class TestGetQtArgs:
 
     """Tests for get_args."""
 
-    def setUp(self):
-        self.parser = qutebrowser.get_argparser()
-        self.parser.exit = argparser_exit
+    @pytest.fixture
+    def parser(self, mocker):
+        """Fixture to provide an argparser.
+
+        Monkey-patches .exit() of the argparser so it doesn't exit on errors.
+        """
+        parser = qutebrowser.get_argparser()
+        mocker.patch.object(parser, 'exit', side_effect=Exception)
+        return parser
 
-    def test_no_qt_args(self):
+    def test_no_qt_args(self, parser):
         """Test commandline with no Qt arguments given."""
-        args = self.parser.parse_args(['--debug'])
-        self.assertEqual(qtutils.get_args(args), [sys.argv[0]])
+        args = parser.parse_args(['--debug'])
+        assert qtutils.get_args(args) == [sys.argv[0]]
 
-    def test_qt_flag(self):
+    def test_qt_flag(self, parser):
         """Test commandline with a Qt flag."""
-        args = self.parser.parse_args(['--debug', '--qt-reverse', '--nocolor'])
-        self.assertEqual(qtutils.get_args(args), [sys.argv[0], '-reverse'])
+        args = parser.parse_args(['--debug', '--qt-reverse', '--nocolor'])
+        assert qtutils.get_args(args) == [sys.argv[0], '-reverse']
 
-    def test_qt_arg(self):
+    def test_qt_arg(self, parser):
         """Test commandline with a Qt argument."""
-        args = self.parser.parse_args(['--qt-stylesheet', 'foobar'])
-        self.assertEqual(qtutils.get_args(args), [sys.argv[0], '-stylesheet',
-                                                  'foobar'])
+        args = parser.parse_args(['--qt-stylesheet', 'foobar'])
+        assert qtutils.get_args(args) == [sys.argv[0], '-stylesheet', 'foobar']
 
-    def test_qt_both(self):
+    def test_qt_both(self, parser):
         """Test commandline with a Qt argument and flag."""
-        args = self.parser.parse_args(['--qt-stylesheet', 'foobar',
-                                       '--qt-reverse'])
+        args = parser.parse_args(['--qt-stylesheet', 'foobar', '--qt-reverse'])
         qt_args = qtutils.get_args(args)
-        self.assertEqual(qt_args[0], sys.argv[0])
-        self.assertIn('-reverse', qt_args)
-        self.assertIn('-stylesheet', qt_args)
-        self.assertIn('foobar', qt_args)
-
-
-if __name__ == '__main__':
-    unittest.main()
+        assert qt_args[0] == sys.argv[0]
+        assert '-reverse' in qt_args
+        assert '-stylesheet' in qt_args
+        assert 'foobar' in qt_args