Repository: vispy/vispy

Issue:
Let's use py.test
You do `from pytest import raises, skip` and that's all you need (you may not even need skip). You can just make regular assertions, and pytest will do some voodoo to give a sensible error message when it fails.  

cc #637 



Discussion:
If we can get `py.test` to do all the stuff we currently do with `nose`, I'm fine with it. I could try to take this on to learn a new testing platform, if nobody else is keen on working on it.

If you do, have a look at imageio for an example. It also has a make.py thing going on, a `run_if_main()` function like ours, and also travis.yml.

I have in my mental queue to also set up OSX and Windows testing, I might tackle these at the same time.


Resolving patch:
diff --git a/make/make.py b/make/make.py
--- a/make/make.py
+++ b/make/make.py
@@ -79,6 +79,7 @@ def coverage_html(self, arg):
         from coverage import coverage
         cov = coverage(auto_data=False, branch=True, data_suffix=None,
                        source=['vispy'])  # should match testing/_coverage.py
+        cov.combine()
         cov.load()
         cov.html_report()
         print('Done, launching browser.')
@@ -172,7 +173,7 @@ def website(self, arg):
     def test(self, arg):
         """ Run tests:
                 * full - run all tests
-                * nose - run nose tests (also for each backend)
+                * unit - run tests (also for each backend)
                 * any backend name (e.g. pyside, pyqt4, glut, sdl2, etc.) -
                   run tests for the given backend
                 * nobackend - run tests that do not require a backend


Test patch:
diff --git a/vispy/app/backends/tests/test_ipynb_util.py b/vispy/app/backends/tests/test_ipynb_util.py
--- a/vispy/app/backends/tests/test_ipynb_util.py
+++ b/vispy/app/backends/tests/test_ipynb_util.py
@@ -2,12 +2,11 @@
 # Copyright (c) 2014, Vispy Development Team.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 import numpy as np
-from nose.tools import assert_equal
 
 from vispy.app.backends._ipynb_util import (_extract_buffers,
                                             _serialize_command,
                                             create_glir_message)
-from vispy.testing import run_tests_if_main
+from vispy.testing import run_tests_if_main, assert_equal
 
 
 def test_extract_buffers():
diff --git a/vispy/app/tests/test_app.py b/vispy/app/tests/test_app.py
--- a/vispy/app/tests/test_app.py
+++ b/vispy/app/tests/test_app.py
@@ -4,12 +4,12 @@
 from time import sleep
 
 from numpy.testing import assert_array_equal
-from nose.tools import assert_equal, assert_true, assert_raises
 
 from vispy.app import use_app, Canvas, Timer, MouseEvent, KeyEvent
 from vispy.app.base import BaseApplicationBackend
 from vispy.testing import (requires_application, SkipTest, assert_is,
-                           assert_in, run_tests_if_main)
+                           assert_in, run_tests_if_main,
+                           assert_equal, assert_true, assert_raises)
 from vispy.util import keys, use_log_level
 
 from vispy.gloo.program import (Program, VertexBuffer, IndexBuffer)
diff --git a/vispy/app/tests/test_backends.py b/vispy/app/tests/test_backends.py
--- a/vispy/app/tests/test_backends.py
+++ b/vispy/app/tests/test_backends.py
@@ -10,11 +10,10 @@
 
 from inspect import getargspec
 
-from nose.tools import assert_raises
-
 import vispy
 from vispy import keys
-from vispy.testing import requires_application, assert_in, run_tests_if_main
+from vispy.testing import (requires_application, assert_in, run_tests_if_main,
+                           assert_raises)
 from vispy.app import use_app, Application
 from vispy.app.backends import _template
 
diff --git a/vispy/app/tests/test_context.py b/vispy/app/tests/test_context.py
--- a/vispy/app/tests/test_context.py
+++ b/vispy/app/tests/test_context.py
@@ -1,8 +1,8 @@
 import os
 import sys
-from nose.tools import assert_equal, assert_raises
 
-from vispy.testing import requires_application, SkipTest, run_tests_if_main
+from vispy.testing import (requires_application, SkipTest, run_tests_if_main,
+                           assert_equal, assert_raises)
 from vispy.app import Canvas, use_app
 from vispy.gloo import get_gl_configuration, Program
 from vispy.gloo.gl import check_error
diff --git a/vispy/app/tests/test_interactive.py b/vispy/app/tests/test_interactive.py
--- a/vispy/app/tests/test_interactive.py
+++ b/vispy/app/tests/test_interactive.py
@@ -1,6 +1,4 @@
-from nose.tools import assert_equal, assert_true, assert_false
-from vispy.testing import assert_in, run_tests_if_main
-
+from vispy.testing import run_tests_if_main
 from vispy.app import set_interactive
 from vispy.ext.ipy_inputhook import inputhook_manager
 
@@ -15,15 +13,15 @@ def test_interactive():
     f = MockApp()
     set_interactive(enabled=True, app=f)
 
-    assert_equal('vispy', inputhook_manager._current_gui)
-    assert_true(f._in_event_loop)
-    assert_in('vispy', inputhook_manager.apps)
-    assert_equal(f, inputhook_manager.apps['vispy'])
+    assert inputhook_manager._current_gui == 'vispy'
+    assert f._in_event_loop
+    assert 'vispy' in inputhook_manager.apps
+    assert f == inputhook_manager.apps['vispy']
 
     set_interactive(enabled=False)
 
-    assert_equal(None, inputhook_manager._current_gui)
-    assert_false(f._in_event_loop)
+    assert inputhook_manager._current_gui is None
+    assert not f._in_event_loop
 
 
 run_tests_if_main()
diff --git a/vispy/app/tests/test_simultaneous.py b/vispy/app/tests/test_simultaneous.py
--- a/vispy/app/tests/test_simultaneous.py
+++ b/vispy/app/tests/test_simultaneous.py
@@ -2,7 +2,6 @@
 
 import numpy as np
 from numpy.testing import assert_allclose
-from nose.tools import assert_true
 from time import sleep
 
 from vispy.app import use_app, Canvas, Timer
@@ -70,8 +69,8 @@ def draw1(event):
             while (ct[0] < n_check or ct[1] < n_check) and time() < timeout:
                 app.process_events()
             print((ct, n_check))
-            assert_true(n_check <= ct[0] <= n_check + 1)
-            assert_true(n_check <= ct[1] <= n_check + 1)
+            assert n_check <= ct[0] <= n_check + 1
+            assert n_check <= ct[1] <= n_check + 1
 
             # check timer
             global timer_ran
@@ -85,7 +84,7 @@ def on_timer(_):
             app.process_events()
             sleep(0.2)
             app.process_events()
-            assert_true(timer_ran)
+            assert timer_ran
 
     if app.backend_name.lower() == 'wx':
         raise SkipTest('wx fails test #2')  # XXX TODO Fix this
diff --git a/vispy/color/tests/test_color.py b/vispy/color/tests/test_color.py
--- a/vispy/color/tests/test_color.py
+++ b/vispy/color/tests/test_color.py
@@ -3,7 +3,6 @@
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 
 import numpy as np
-from nose.tools import assert_equal, assert_raises, assert_true
 from numpy.testing import assert_array_equal, assert_allclose
 
 from vispy.color import (Color, ColorArray, get_color_names,
@@ -11,7 +10,8 @@
                          get_color_dict, get_colormap, get_colormaps)
 from vispy.visuals.shaders import Function
 from vispy.util import use_log_level
-from vispy.testing import run_tests_if_main
+from vispy.testing import (run_tests_if_main, assert_equal, assert_raises,
+                           assert_true)
 
 
 def test_color():
diff --git a/vispy/gloo/gl/tests/test_basics.py b/vispy/gloo/gl/tests/test_basics.py
--- a/vispy/gloo/gl/tests/test_basics.py
+++ b/vispy/gloo/gl/tests/test_basics.py
@@ -7,12 +7,11 @@
 
 import sys
 
-from nose.tools import assert_equal, assert_true  # noqa
-
 from vispy.app import Canvas
 from numpy.testing import assert_almost_equal
 from vispy.testing import (requires_application, requires_pyopengl, SkipTest,
-                           glut_skip, run_tests_if_main)
+                           glut_skip, run_tests_if_main, assert_equal,
+                           assert_true)
 from vispy.ext.six import string_types
 from vispy.util import use_log_level
 from vispy.gloo import gl
diff --git a/vispy/gloo/gl/tests/test_functionality.py b/vispy/gloo/gl/tests/test_functionality.py
--- a/vispy/gloo/gl/tests/test_functionality.py
+++ b/vispy/gloo/gl/tests/test_functionality.py
@@ -12,18 +12,16 @@
 blue). The drawing is done for 50% using attribute data, and 50%
 using a texture. The end result should be fully saturated colors.
 
-Remember: the bottom left is (-1, -1) and the first quadrant. 
-
+Remember: the bottom left is (-1, -1) and the first quadrant.
 """
 import sys
 
 import numpy as np
 
-from nose.tools import assert_equal, assert_true
 from vispy.app import Canvas
-from numpy.testing import assert_almost_equal  # noqa
 from vispy.testing import (requires_application, requires_pyopengl, SkipTest,
-                           glut_skip, run_tests_if_main)
+                           glut_skip, run_tests_if_main, assert_equal,
+                           assert_true)
 
 from vispy.gloo import gl
 
diff --git a/vispy/gloo/gl/tests/test_names.py b/vispy/gloo/gl/tests/test_names.py
--- a/vispy/gloo/gl/tests/test_names.py
+++ b/vispy/gloo/gl/tests/test_names.py
@@ -2,7 +2,6 @@
 backends, and no more than that.
 """
 
-from nose.tools import assert_equal
 from vispy.testing import requires_pyopengl
 
 from vispy.gloo import gl
@@ -23,14 +22,14 @@ def __getattr__(self, name):
 
 def _test_function_names(mod):
     fnames = set([name for name in dir(mod) if name.startswith('gl')])
-    assert_equal(function_names.difference(fnames), set())
-    assert_equal(fnames.difference(function_names), set())
+    assert function_names.difference(fnames) == set()
+    assert fnames.difference(function_names) == set()
 
 
 def _test_constant_names(mod):
     cnames = set([name for name in dir(mod) if name.startswith('GL')])
-    assert_equal(constant_names.difference(cnames), set())
-    assert_equal(cnames.difference(constant_names), set())
+    assert constant_names.difference(cnames) == set()
+    assert cnames.difference(constant_names) == set()
 
 
 def test_destop():
diff --git a/vispy/gloo/tests/test_context.py b/vispy/gloo/tests/test_context.py
--- a/vispy/gloo/tests/test_context.py
+++ b/vispy/gloo/tests/test_context.py
@@ -2,8 +2,8 @@
 
 import gc
 
-from nose.tools import assert_raises, assert_equal, assert_not_equal
-from vispy.testing import assert_in, run_tests_if_main
+from vispy.testing import (assert_in, run_tests_if_main, assert_raises,
+                           assert_equal, assert_not_equal)
 
 from vispy.gloo import (GLContext, get_default_config)
 
diff --git a/vispy/gloo/tests/test_framebuffer.py b/vispy/gloo/tests/test_framebuffer.py
--- a/vispy/gloo/tests/test_framebuffer.py
+++ b/vispy/gloo/tests/test_framebuffer.py
@@ -1,10 +1,9 @@
 # -*- coding: utf-8 -*-
 
-from vispy.testing import run_tests_if_main
+from vispy.testing import run_tests_if_main, assert_raises
 
 from vispy import gloo
 from vispy.gloo import FrameBuffer, RenderBuffer
-from nose.tools import assert_raises
 
 
 def test_renderbuffer():
diff --git a/vispy/gloo/tests/test_texture.py b/vispy/gloo/tests/test_texture.py
--- a/vispy/gloo/tests/test_texture.py
+++ b/vispy/gloo/tests/test_texture.py
@@ -7,7 +7,7 @@
 import numpy as np
 
 from vispy.gloo import Texture2D, Texture3D, TextureAtlas
-from vispy.testing import requires_pyopengl, run_tests_if_main
+from vispy.testing import requires_pyopengl, run_tests_if_main, assert_raises
 
 # here we test some things that will be true of all Texture types:
 Texture = Texture2D
@@ -306,149 +306,134 @@ def test_reset_data_type(self):
 
 
 # --------------------------------------------------------------- Texture3D ---
-class Texture3DTest(unittest.TestCase):
+@requires_pyopengl()
+def test_texture_3D():
     # Note: put many tests related to (re)sizing here, because Texture
     # is not really aware of shape.
 
-    @requires_pyopengl()
-    def __init__(self, *args, **kwds):
-        unittest.TestCase.__init__(self, *args, **kwds)
-
     # Shape extension
     # ---------------------------------
-    def test_init(self):
-        data = np.zeros((10, 10, 10), dtype=np.uint8)
-        T = Texture3D(data=data)
-        assert T._shape == (10, 10, 10, 1)
-        assert 'Texture3D' in repr(T)
-        assert T.glsl_type == ('uniform', 'sampler3D')
+    data = np.zeros((10, 10, 10), dtype=np.uint8)
+    T = Texture3D(data=data)
+    assert T._shape == (10, 10, 10, 1)
+    assert 'Texture3D' in repr(T)
+    assert T.glsl_type == ('uniform', 'sampler3D')
         
     # Width & height
     # ---------------------------------
-    def test_width_height_depth(self):
-        data = np.zeros((10, 20, 30), dtype=np.uint8)
-        T = Texture3D(data=data)
-        assert T.width == 30
-        assert T.height == 20
-        assert T.depth == 10
+    data = np.zeros((10, 20, 30), dtype=np.uint8)
+    T = Texture3D(data=data)
+    assert T.width == 30
+    assert T.height == 20
+    assert T.depth == 10
 
     # Resize
     # ---------------------------------
-    def test_resize(self):
-        data = np.zeros((10, 10, 10), dtype=np.uint8)
-        T = Texture3D(data=data)
-        T.resize((5, 5, 5))
-        assert T.shape == (5, 5, 5, 1)
-        glir_cmd = T._glir.clear()[-1]
-        assert glir_cmd[0] == 'SIZE'
-        assert glir_cmd[2] == (5, 5, 5, 1)
+    data = np.zeros((10, 10, 10), dtype=np.uint8)
+    T = Texture3D(data=data)
+    T.resize((5, 5, 5))
+    assert T.shape == (5, 5, 5, 1)
+    glir_cmd = T._glir.clear()[-1]
+    assert glir_cmd[0] == 'SIZE'
+    assert glir_cmd[2] == (5, 5, 5, 1)
     
     # Resize with bad shape
     # ---------------------------------
-    def test_resize_bad_shape(self):
-        data = np.zeros((10, 10, 10), dtype=np.uint8)
-        T = Texture3D(data=data)
-        # with self.assertRaises(ValueError):
-        #    T.resize((5, 5, 5, 5))
-        self.assertRaises(ValueError, T.resize, (5, 5, 5, 5))
+    data = np.zeros((10, 10, 10), dtype=np.uint8)
+    T = Texture3D(data=data)
+    # with self.assertRaises(ValueError):
+    #    T.resize((5, 5, 5, 5))
+    assert_raises(ValueError, T.resize, (5, 5, 5, 5))
 
     # Resize not resizeable
     # ---------------------------------
-    def test_resize_unresizeable(self):
-        data = np.zeros((10, 10, 10), dtype=np.uint8)
-        T = Texture3D(data=data, resizeable=False)
-        # with self.assertRaises(RuntimeError):
-        #    T.resize((5, 5, 5))
-        self.assertRaises(RuntimeError, T.resize, (5, 5, 5))
+    data = np.zeros((10, 10, 10), dtype=np.uint8)
+    T = Texture3D(data=data, resizeable=False)
+    # with self.assertRaises(RuntimeError):
+    #    T.resize((5, 5, 5))
+    assert_raises(RuntimeError, T.resize, (5, 5, 5))
 
     # Set oversized data (-> resize)
     # ---------------------------------
-    def test_set_oversized_data(self):
-        data = np.zeros((10, 10, 10), dtype=np.uint8)
-        T = Texture3D(data=data)
-        T.set_data(np.ones((20, 20, 20), np.uint8))
-        assert T.shape == (20, 20, 20, 1)
+    data = np.zeros((10, 10, 10), dtype=np.uint8)
+    T = Texture3D(data=data)
+    T.set_data(np.ones((20, 20, 20), np.uint8))
+    assert T.shape == (20, 20, 20, 1)
         
     # Set undersized data
     # ---------------------------------
-    def test_set_undersized_data(self):
-        data = np.zeros((10, 10, 10), dtype=np.uint8)
-        T = Texture3D(data=data)
-        T.set_data(np.ones((5, 5, 5), np.uint8))
-        assert T.shape == (5, 5, 5, 1)
+    data = np.zeros((10, 10, 10), dtype=np.uint8)
+    T = Texture3D(data=data)
+    T.set_data(np.ones((5, 5, 5), np.uint8))
+    assert T.shape == (5, 5, 5, 1)
 
     # Set misplaced data
     # ---------------------------------
-    def test_set_misplaced_data(self):
-        data = np.zeros((10, 10, 10), dtype=np.uint8)
-        T = Texture3D(data=data)
-        # with self.assertRaises(ValueError):
-        #    T.set_data(np.ones((5, 5, 5)), offset=(8, 8, 8))
-        self.assertRaises(ValueError, T.set_data,
-                          np.ones((5, 5, 5)), offset=(8, 8, 8))
+    data = np.zeros((10, 10, 10), dtype=np.uint8)
+    T = Texture3D(data=data)
+    # with self.assertRaises(ValueError):
+    #    T.set_data(np.ones((5, 5, 5)), offset=(8, 8, 8))
+    assert_raises(ValueError, T.set_data,
+                  np.ones((5, 5, 5)), offset=(8, 8, 8))
 
     # Set misshaped data
     # ---------------------------------
-    def test_set_misshaped_data_3D(self):
-        data = np.zeros((10, 10, 10), dtype=np.uint8)
-        T = Texture3D(data=data)
-        # with self.assertRaises(ValueError):
-        #    T.set_data(np.ones((10, 10, 10)))
-        self.assertRaises(ValueError, T.set_data, np.ones((10,)))
+    data = np.zeros((10, 10, 10), dtype=np.uint8)
+    T = Texture3D(data=data)
+    # with self.assertRaises(ValueError):
+    #    T.set_data(np.ones((10, 10, 10)))
+    assert_raises(ValueError, T.set_data, np.ones((10,)))
 
     # Set whole data (clear pending data)
     # ---------------------------------
-    def test_set_whole_data(self):
-        data = np.zeros((10, 10, 10), dtype=np.uint8)
-        T = Texture3D(data=data)
-        T.set_data(np.ones((10, 10, 10), np.uint8))
-        assert T.shape == (10, 10, 10, 1)
-        glir_cmd = T._glir.clear()[-1]
-        assert glir_cmd[0] == 'DATA'
+    data = np.zeros((10, 10, 10), dtype=np.uint8)
+    T = Texture3D(data=data)
+    T.set_data(np.ones((10, 10, 10), np.uint8))
+    assert T.shape == (10, 10, 10, 1)
+    glir_cmd = T._glir.clear()[-1]
+    assert glir_cmd[0] == 'DATA'
 
     # Test set data with different shape
     # ---------------------------------
-    def test_reset_data_shape(self):
-        shape1 = 10, 10, 10
-        shape3 = 10, 10, 10, 3
-        
-        # Init data (explicit shape)
-        data = np.zeros((10, 10, 10, 1), dtype=np.uint8)
-        T = Texture3D(data=data)
-        assert T.shape == (10, 10, 10, 1)
-        assert T._format == 'luminance'
-        
-        # Set data to rgb
-        T.set_data(np.zeros(shape3, np.uint8))
-        assert T.shape == (10, 10, 10, 3)
-        assert T._format == 'rgb'
-        
-        # Set data to grayscale
-        T.set_data(np.zeros(shape1, np.uint8))
-        assert T.shape == (10, 10, 10, 1)
-        assert T._format == 'luminance'
-        
-        # Set size to rgb
-        T.resize(shape3)
-        assert T.shape == (10, 10, 10, 3)
-        assert T._format == 'rgb'
-        
-        # Set size to grayscale
-        T.resize(shape1)
-        assert T.shape == (10, 10, 10, 1)
-        assert T._format == 'luminance'
+    shape1 = 10, 10, 10
+    shape3 = 10, 10, 10, 3
+    
+    # Init data (explicit shape)
+    data = np.zeros((10, 10, 10, 1), dtype=np.uint8)
+    T = Texture3D(data=data)
+    assert T.shape == (10, 10, 10, 1)
+    assert T._format == 'luminance'
+    
+    # Set data to rgb
+    T.set_data(np.zeros(shape3, np.uint8))
+    assert T.shape == (10, 10, 10, 3)
+    assert T._format == 'rgb'
+    
+    # Set data to grayscale
+    T.set_data(np.zeros(shape1, np.uint8))
+    assert T.shape == (10, 10, 10, 1)
+    assert T._format == 'luminance'
+    
+    # Set size to rgb
+    T.resize(shape3)
+    assert T.shape == (10, 10, 10, 3)
+    assert T._format == 'rgb'
+    
+    # Set size to grayscale
+    T.resize(shape1)
+    assert T.shape == (10, 10, 10, 1)
+    assert T._format == 'luminance'
 
     # Test set data with different shape and type
     # -------------------------------------------
-    def test_reset_data_type(self):
-        data = np.zeros((10, 10, 10), dtype=np.uint8)
-        T = Texture3D(data=data)
-        
-        data = np.zeros((10, 11, 11), dtype=np.float32)
-        T.set_data(data)
-        
-        data = np.zeros((12, 12, 10), dtype=np.int32)
-        T.set_data(data)
+    data = np.zeros((10, 10, 10), dtype=np.uint8)
+    T = Texture3D(data=data)
+    
+    data = np.zeros((10, 11, 11), dtype=np.float32)
+    T.set_data(data)
+    
+    data = np.zeros((12, 12, 10), dtype=np.int32)
+    T.set_data(data)
 
 
 class TextureAtlasTest(unittest.TestCase):
diff --git a/vispy/gloo/tests/test_use_gloo.py b/vispy/gloo/tests/test_use_gloo.py
--- a/vispy/gloo/tests/test_use_gloo.py
+++ b/vispy/gloo/tests/test_use_gloo.py
@@ -5,13 +5,14 @@
 # -----------------------------------------------------------------------------
 import numpy as np
 from numpy.testing import assert_allclose
-from nose.tools import assert_raises, assert_equal
 
 from vispy.app import Canvas
 from vispy.gloo import (Texture2D, Texture3D, Program, FrameBuffer,
                         RenderBuffer, set_viewport, clear)
 from vispy.gloo.util import draw_texture, _screenshot
-from vispy.testing import requires_application, has_pyopengl, run_tests_if_main
+from vispy.testing import (requires_application, has_pyopengl,
+                           run_tests_if_main,
+                           assert_raises, assert_equal)
 
 
 @requires_application()
diff --git a/vispy/gloo/tests/test_util.py b/vispy/gloo/tests/test_util.py
--- a/vispy/gloo/tests/test_util.py
+++ b/vispy/gloo/tests/test_util.py
@@ -2,8 +2,7 @@
 
 from vispy.gloo import util
 
-from vispy.testing import run_tests_if_main
-from nose.tools import assert_raises
+from vispy.testing import run_tests_if_main, assert_raises
 
 
 def test_check_enum():
diff --git a/vispy/gloo/tests/test_wrappers.py b/vispy/gloo/tests/test_wrappers.py
--- a/vispy/gloo/tests/test_wrappers.py
+++ b/vispy/gloo/tests/test_wrappers.py
@@ -5,12 +5,12 @@
 # -----------------------------------------------------------------------------
 import numpy as np
 from numpy.testing import assert_array_equal, assert_allclose
-from nose.tools import assert_true, assert_equal, assert_raises
 
 from vispy import gloo
 from vispy.gloo import gl
 from vispy.app import Canvas
-from vispy.testing import requires_application, run_tests_if_main
+from vispy.testing import (requires_application, run_tests_if_main,
+                           assert_true, assert_equal, assert_raises)
 from vispy.gloo import read_pixels
 from vispy.gloo.glir import GlirQueue
 from vispy.gloo import wrappers
diff --git a/vispy/io/tests/test_io.py b/vispy/io/tests/test_io.py
--- a/vispy/io/tests/test_io.py
+++ b/vispy/io/tests/test_io.py
@@ -3,13 +3,12 @@
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 import numpy as np
 from os import path as op
-from nose.tools import assert_equal, assert_raises
 from numpy.testing import assert_allclose, assert_array_equal
 
 from vispy.io import write_mesh, read_mesh, load_data_file
 from vispy.geometry import _fast_cross_3d
 from vispy.util import _TempDir
-from vispy.testing import run_tests_if_main
+from vispy.testing import run_tests_if_main, assert_equal, assert_raises
 
 temp_dir = _TempDir()
 
diff --git a/vispy/mpl_plot/tests/test_show_vispy.py b/vispy/mpl_plot/tests/test_show_vispy.py
--- a/vispy/mpl_plot/tests/test_show_vispy.py
+++ b/vispy/mpl_plot/tests/test_show_vispy.py
@@ -3,11 +3,10 @@
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 
 import numpy as np
-from nose.tools import assert_raises
 
 from vispy.io import read_png, load_data_file
 from vispy.testing import (has_matplotlib, requires_application,
-                           run_tests_if_main)
+                           run_tests_if_main, assert_raises)
 import vispy.mpl_plot as plt
 
 
diff --git a/vispy/testing/__init__.py b/vispy/testing/__init__.py
--- a/vispy/testing/__init__.py
+++ b/vispy/testing/__init__.py
@@ -3,9 +3,10 @@
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 
 from ._testing import (SkipTest, requires_application, requires_img_lib,  # noqa
-                      assert_is, assert_in, assert_not_in, has_backend,  # noqa
-                      glut_skip, requires_pyopengl, requires_scipy,  # noqa
-                      has_matplotlib, assert_image_equal,  # noqa
+                      has_backend, glut_skip, requires_pyopengl,  # noqa
+                      requires_scipy, has_matplotlib,  # noqa
                       save_testing_image, TestingCanvas, has_pyopengl,  # noqa
-                      run_tests_if_main)  # noqa
+                      run_tests_if_main, assert_image_equal,
+                      assert_is, assert_in, assert_not_in, assert_equal,
+                      assert_not_equal, assert_raises, assert_true)  # noqa
 from ._runners import test  # noqa
diff --git a/vispy/testing/_runners.py b/vispy/testing/_runners.py
--- a/vispy/testing/_runners.py
+++ b/vispy/testing/_runners.py
@@ -31,71 +31,31 @@ def _get_root_dir():
     return root_dir, dev
 
 
-_nose_script = """
-# Code inspired by original nose plugin:
-# https://nose.readthedocs.org/en/latest/plugins/cover.html
-
-import nose
-from nose.plugins.base import Plugin
-
-
-class MutedCoverage(Plugin):
-    '''Make a silent coverage report using Ned Batchelder's coverage module.'''
-
-    def configure(self, options, conf):
-        Plugin.configure(self, options, conf)
-        self.enabled = True
-        try:
-            from coverage import coverage
-        except ImportError:
-            self.enabled = False
-            self.cov = None
-            print('Module "coverage" not installed, code coverage will not '
-                  'be available')
-        else:
-            self.enabled = True
-            self.cov = coverage(auto_data=False, branch=True, data_suffix=None,
-                                source=['vispy'])
-
-    def begin(self):
-        self.cov.load()
-        self.cov.start()
-
-    def report(self, stream):
-        self.cov.stop()
-        self.cov.combine()
-        self.cov.save()
-
-
+_unit_script = """
+import pytest
 try:
     import faulthandler
     faulthandler.enable()
 except Exception:
     pass
 
-nose.main(argv=%r%s)
+raise SystemExit(pytest.main(%r))
 """
 
 
-def _nose(mode, extra_arg_string):
-    """Run nosetests using a particular mode"""
-    cwd = os.getcwd()  # this must be done before nose import
+def _unit(mode, extra_arg_string):
+    """Run unit tests using a particular mode"""
+    cwd = os.getcwd()
     try:
-        import nose  # noqa, analysis:ignore
+        import pytest  # noqa, analysis:ignore
     except ImportError:
-        print('Skipping nosetests, nose not installed')
+        print('Skipping pytest, pytest not installed')
         raise SkipTest()
 
     if mode == 'nobackend':
         msg = 'Running tests with no backend'
-        extra_arg_string = '-a !vispy_app_test ' + extra_arg_string
-        extra_arg_string = '-e experimental -e wiki ' + extra_arg_string
+        extra_arg_string = '-m "not vispy_app_test" ' + extra_arg_string
         coverage = True
-    elif mode == 'singlefile':
-        fname = extra_arg_string.split(' ')[0]
-        assert op.isfile(fname)
-        msg = 'Running tests for individual file'
-        coverage = False
     else:
         with use_log_level('warning', print_msg=False):
             has, why_not = has_backend(mode, out=['why_not'])
@@ -105,30 +65,34 @@ def _nose(mode, extra_arg_string):
             print(_line_sep + '\n' + msg + '\n' + _line_sep + '\n')
             raise SkipTest(msg)
         msg = 'Running tests with %s backend' % mode
-        extra_arg_string = '-a vispy_app_test ' + extra_arg_string
+        extra_arg_string = '-m vispy_app_test ' + extra_arg_string
         coverage = True
-    coverage = ', addplugins=[MutedCoverage()]' if coverage else ''
-    args = ['nosetests'] + extra_arg_string.strip().split(' ')
+    if coverage:
+        extra_arg_string += (' --cov vispy --cov-report=term-missing '
+                             '--no-cov-on-fail ')
     # make a call to "python" so that it inherits whatever the system
     # thinks is "python" (e.g., virtualenvs)
-    cmd = [sys.executable, '-c', _nose_script % (args, coverage)]
+    cmd = [sys.executable, '-c', _unit_script % extra_arg_string]
     env = deepcopy(os.environ)
-    if mode in ('singlefile',):
-        env_str = ''
-    else:
-        # We want to set this for all app backends plus "nobackend" to
-        # help ensure that app tests are appropriately decorated
-        env.update(dict(_VISPY_TESTING_APP=mode))
-        env_str = '_VISPY_TESTING_APP=%s ' % mode
+
+    # We want to set this for all app backends plus "nobackend" to
+    # help ensure that app tests are appropriately decorated
+    env.update(dict(_VISPY_TESTING_APP=mode))
+    env_str = '_VISPY_TESTING_APP=%s ' % mode
     if len(msg) > 0:
         msg = ('%s\n%s:\n%s%s'
-               % (_line_sep, msg, env_str, ' '.join(args)))
+               % (_line_sep, msg, env_str, extra_arg_string))
         print(msg)
     sys.stdout.flush()
     return_code = run_subprocess(cmd, return_code=True, cwd=cwd, env=env,
                                  stdout=None, stderr=None)[2]
     if return_code:
-        raise RuntimeError('Nose failure (%s)' % return_code)
+        raise RuntimeError('unit failure (%s)' % return_code)
+    else:
+        out_name = '.coverage.%s' % mode
+        if op.isfile(out_name):
+            os.remove(out_name)
+        os.rename('.coverage', out_name)
 
 
 def _flake():
@@ -304,34 +268,33 @@ def test(label='full', extra_arg_string=''):
     Parameters
     ----------
     label : str
-        Can be one of 'full', 'nose', 'nobackend', 'extra', 'lineendings',
+        Can be one of 'full', 'unit', 'nobackend', 'extra', 'lineendings',
         'flake', or any backend name (e.g., 'qt').
     extra_arg_string : str
-        Extra arguments to sent to ``nose``, e.g. ``'-x --verbosity=2'``.
+        Extra arguments to sent to ``pytest``.
     """
     from vispy.app.backends import BACKEND_NAMES as backend_names
     label = label.lower()
-    if op.isfile('.coverage'):
-        os.remove('.coverage')
-    known_types = ['full', 'nose', 'lineendings', 'extra', 'flake',
-                   'nobackend', 'examples'] + backend_names
-    if label not in known_types:
-        raise ValueError('label must be one of %s, or a backend name %s'
-                         % (known_types, backend_names))
+    label = 'pytest' if label == 'nose' else label
+    known_types = ['full', 'unit', 'lineendings', 'extra', 'flake',
+                   'nobackend', 'examples']
+    if label not in known_types + backend_names:
+        raise ValueError('label must be one of %s, or a backend name %s, '
+                         'not \'%s\'' % (known_types, backend_names, label))
     work_dir = _get_root_dir()[0]
     orig_dir = os.getcwd()
     # figure out what we actually need to run
     runs = []
-    if label in ('full', 'nose'):
+    if label in ('full', 'unit'):
         for backend in backend_names:
-            runs.append([partial(_nose, backend, extra_arg_string),
+            runs.append([partial(_unit, backend, extra_arg_string),
                          backend])
     elif label in backend_names:
-        runs.append([partial(_nose, label, extra_arg_string), label])
+        runs.append([partial(_unit, label, extra_arg_string), label])
     if label in ('full', 'examples'):
         runs.append([_examples, 'examples'])
-    if label in ('full', 'nose', 'nobackend'):
-        runs.append([partial(_nose, 'nobackend', extra_arg_string),
+    if label in ('full', 'unit', 'nobackend'):
+        runs.append([partial(_unit, 'nobackend', extra_arg_string),
                      'nobackend'])
     if label in ('full', 'extra', 'lineendings'):
         runs.append([_check_line_endings, 'lineendings'])
diff --git a/vispy/testing/_testing.py b/vispy/testing/_testing.py
--- a/vispy/testing/_testing.py
+++ b/vispy/testing/_testing.py
@@ -11,14 +11,6 @@
 import os
 import inspect
 import base64
-try:
-    from nose.tools import nottest, assert_equal, assert_true
-except ImportError:
-    assert_equal = assert_true = None
-
-    class nottest(object):
-        def __init__(self, *args):
-            pass  # Avoid "object() takes no parameters"
 
 from distutils.version import LooseVersion
 
@@ -28,7 +20,7 @@ def __init__(self, *args):
 from ..util import use_log_level, run_subprocess, get_testing_file
 
 ###############################################################################
-# Adapted from Python's unittest2 (which is wrapped by nose)
+# Adapted from Python's unittest2
 # http://docs.python.org/2/license.html
 
 try:
@@ -72,26 +64,65 @@ def _format_msg(msg, std_msg):
     return msg
 
 
+def nottest(func):
+    """Decorator to mark a function or method as *not* a test
+    """
+    func.__test__ = False
+    return func
+
+
+def assert_raises(exp, func, *args, **kwargs):
+    """Backport"""
+    try:
+        func(*args, **kwargs)
+    except exp:
+        return
+    std_msg = '%s not raised' % (_safe_rep(exp))
+    raise AssertionError(_format_msg(None, std_msg))
+
+
 def assert_in(member, container, msg=None):
-    """Backport for old nose.tools"""
+    """Backport"""
     if member in container:
         return
     std_msg = '%s not found in %s' % (_safe_rep(member), _safe_rep(container))
-    msg = _format_msg(msg, std_msg)
-    raise AssertionError(msg)
+    raise AssertionError(_format_msg(msg, std_msg))
+
+
+def assert_true(x, msg=None):
+    """Backport"""
+    if x:
+        return
+    std_msg = '%s is not True' % (_safe_rep(x),)
+    raise AssertionError(_format_msg(msg, std_msg))
+
+
+def assert_equal(x, y, msg=None):
+    """Backport"""
+    if x == y:
+        return
+    std_msg = '%s not equal to %s' % (_safe_rep(x), _safe_rep(y))
+    raise AssertionError(_format_msg(msg, std_msg))
+
+
+def assert_not_equal(x, y, msg=None):
+    """Backport"""
+    if x != y:
+        return
+    std_msg = '%s equal to %s' % (_safe_rep(x), _safe_rep(y))
+    raise AssertionError(_format_msg(msg, std_msg))
 
 
 def assert_not_in(member, container, msg=None):
-    """Backport for old nose.tools"""
+    """Backport"""
     if member not in container:
         return
     std_msg = '%s found in %s' % (_safe_rep(member), _safe_rep(container))
-    msg = _format_msg(msg, std_msg)
-    raise AssertionError(msg)
+    raise AssertionError(_format_msg(msg, std_msg))
 
 
 def assert_is(expr1, expr2, msg=None):
-    """Backport for old nose.tools"""
+    """Backport"""
     if expr1 is not expr2:
         std_msg = '%s is not %s' % (_safe_rep(expr1), _safe_rep(expr2))
         raise AssertionError(_format_msg(msg, std_msg))
@@ -165,21 +196,24 @@ def has_application(backend=None, has=(), capable=()):
     return good, msg
 
 
+def composed(*decs):
+    def deco(f):
+        for dec in reversed(decs):
+            f = dec(f)
+        return f
+    return deco
+
+
 def requires_application(backend=None, has=(), capable=()):
     """Return a decorator for tests that require an application"""
     good, msg = has_application(backend, has, capable)
-
-    def skip_decorator(f):
-        import nose
-        f.vispy_app_test = True  # set attribute for easy run or not
-
-        def skipper(*args, **kwargs):
-            if not good:
-                raise SkipTest("Skipping test: %s: %s" % (f.__name__, msg))
-            else:
-                return f(*args, **kwargs)
-        return nose.tools.make_decorator(f)(skipper)
-    return skip_decorator
+    dec_backend = np.testing.dec.skipif(not good, "Skipping test: %s" % msg)
+    try:
+        import pytest
+    except Exception:
+        return dec_backend
+    dec_app = pytest.mark.vispy_app_test
+    return composed(dec_app, dec_backend)
 
 
 def glut_skip():
@@ -354,30 +388,23 @@ def save_testing_image(image, location):
 
 
 @nottest
-def run_tests_if_main(nose=False):
+def run_tests_if_main():
     """Run tests in a given file if it is run as a script"""
     local_vars = inspect.currentframe().f_back.f_locals
     if not local_vars.get('__name__', '') == '__main__':
         return
     # we are in a "__main__"
     fname = local_vars['__file__']
-    if nose:
-        # Run using nose. More similar to normal test
-        if not os.path.isfile(fname):
-            raise IOError('Could not find file "%s"' % fname)
-        from ._runners import _nose
-        _nose('singlefile', fname + ' --verbosity=2')
-    else:
-        # Run ourselves. post-mortem debugging!
-        try:
-            import faulthandler
-            faulthandler.enable()
-        except Exception:
-            pass
-        import __main__
-        print('==== Running tests in script\n==== %s' % fname)
-        run_tests_in_object(__main__)
-        print('==== Tests pass')
+    # Run ourselves. post-mortem debugging!
+    try:
+        import faulthandler
+        faulthandler.enable()
+    except Exception:
+        pass
+    import __main__
+    print('==== Running tests in script\n==== %s' % fname)
+    run_tests_in_object(__main__)
+    print('==== Tests pass')
 
 
 def run_tests_in_object(ob):
diff --git a/vispy/testing/tests/test_testing.py b/vispy/testing/tests/test_testing.py
--- a/vispy/testing/tests/test_testing.py
+++ b/vispy/testing/tests/test_testing.py
@@ -3,9 +3,8 @@
 # Copyright (c) 2014, Vispy Development Team. All Rights Reserved.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 # -----------------------------------------------------------------------------
-from nose.tools import assert_raises
 from vispy.testing import (assert_in, assert_not_in, assert_is,
-                           run_tests_if_main)
+                           run_tests_if_main, assert_raises)
 
 
 def test_testing():
diff --git a/vispy/util/dpi/tests/test_dpi.py b/vispy/util/dpi/tests/test_dpi.py
--- a/vispy/util/dpi/tests/test_dpi.py
+++ b/vispy/util/dpi/tests/test_dpi.py
@@ -2,8 +2,6 @@
 # Copyright (c) 2014, Vispy Development Team.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 
-from nose.tools import assert_true
-
 from vispy.util.dpi import get_dpi
 from vispy.testing import run_tests_if_main
 
@@ -11,8 +9,8 @@
 def test_dpi():
     """Test dpi support"""
     dpi = get_dpi()
-    assert_true(dpi > 0.)
-    assert_true(isinstance(dpi, float))
+    assert dpi > 0.
+    assert isinstance(dpi, float)
 
 
 run_tests_if_main()
diff --git a/vispy/util/tests/test_config.py b/vispy/util/tests/test_config.py
--- a/vispy/util/tests/test_config.py
+++ b/vispy/util/tests/test_config.py
@@ -1,13 +1,13 @@
 # -*- coding: utf-8 -*-
 # Copyright (c) 2014, Vispy Development Team.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
-from nose.tools import assert_raises, assert_equal, assert_true
 from os import path as op
 import os
 
 from vispy.util import (config, sys_info, _TempDir, set_data_dir, save_config,
                         load_data_file)
-from vispy.testing import assert_in, requires_application, run_tests_if_main
+from vispy.testing import (assert_in, requires_application, run_tests_if_main,
+                           assert_raises, assert_equal, assert_true)
 temp_dir = _TempDir()
 
 
diff --git a/vispy/util/tests/test_emitter_group.py b/vispy/util/tests/test_emitter_group.py
--- a/vispy/util/tests/test_emitter_group.py
+++ b/vispy/util/tests/test_emitter_group.py
@@ -3,11 +3,10 @@
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 import unittest
 import copy
-from nose.tools import assert_true, assert_raises
 
 from vispy.util.event import Event, EventEmitter, EmitterGroup
 from vispy.util import use_log_level
-from vispy.testing import run_tests_if_main
+from vispy.testing import run_tests_if_main, assert_true, assert_raises
 
 
 class BasicEvent(Event):
diff --git a/vispy/util/tests/test_event_emitter.py b/vispy/util/tests/test_event_emitter.py
--- a/vispy/util/tests/test_event_emitter.py
+++ b/vispy/util/tests/test_event_emitter.py
@@ -1,13 +1,12 @@
 # -*- coding: utf-8 -*-
 # Copyright (c) 2014, Vispy Development Team.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
-from nose.tools import assert_raises, assert_equal
 import unittest
 import copy
 import functools
 
 from vispy.util.event import Event, EventEmitter
-from vispy.testing import run_tests_if_main
+from vispy.testing import run_tests_if_main, assert_raises, assert_equal
 
 
 class BasicEvent(Event):
diff --git a/vispy/util/tests/test_import.py b/vispy/util/tests/test_import.py
--- a/vispy/util/tests/test_import.py
+++ b/vispy/util/tests/test_import.py
@@ -9,9 +9,8 @@
 import sys
 import os
 
-from nose.tools import assert_equal
 from vispy.testing import (assert_in, assert_not_in, requires_pyopengl,
-                           run_tests_if_main)
+                           run_tests_if_main, assert_equal)
 from vispy.util import run_subprocess
 import vispy
 
diff --git a/vispy/util/tests/test_key.py b/vispy/util/tests/test_key.py
--- a/vispy/util/tests/test_key.py
+++ b/vispy/util/tests/test_key.py
@@ -1,10 +1,10 @@
 # -*- coding: utf-8 -*-
 # Copyright (c) 2014, Vispy Development Team.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
-from nose.tools import assert_raises, assert_true, assert_equal
 
 from vispy.util.keys import Key, ENTER
-from vispy.testing import run_tests_if_main
+from vispy.testing import (run_tests_if_main, assert_raises, assert_true,
+                           assert_equal)
 
 
 def test_key():
diff --git a/vispy/util/tests/test_logging.py b/vispy/util/tests/test_logging.py
--- a/vispy/util/tests/test_logging.py
+++ b/vispy/util/tests/test_logging.py
@@ -1,11 +1,11 @@
 # -*- coding: utf-8 -*-
 # Copyright (c) 2014, Vispy Development Team.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
-from nose.tools import assert_equal
 import logging
 
 from vispy.util import logger, use_log_level
-from vispy.testing import assert_in, assert_not_in, run_tests_if_main
+from vispy.testing import (assert_in, assert_not_in, run_tests_if_main,
+                           assert_equal)
 
 
 def test_logging():
diff --git a/vispy/util/tests/test_run.py b/vispy/util/tests/test_run.py
--- a/vispy/util/tests/test_run.py
+++ b/vispy/util/tests/test_run.py
@@ -1,10 +1,8 @@
 # -*- coding: utf-8 -*-
 # Copyright (c) 2014, Vispy Development Team.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
-from nose.tools import assert_raises
-
 from vispy.util import run_subprocess
-from vispy.testing import run_tests_if_main
+from vispy.testing import run_tests_if_main, assert_raises
 
 
 def test_run():
diff --git a/vispy/util/tests/test_transforms.py b/vispy/util/tests/test_transforms.py
--- a/vispy/util/tests/test_transforms.py
+++ b/vispy/util/tests/test_transforms.py
@@ -2,13 +2,12 @@
 # Copyright (c) 2014, Vispy Development Team.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 import numpy as np
-from nose.tools import assert_equal
 from numpy.testing import assert_allclose
 
 from vispy.util.transforms import (translate, scale, xrotate, yrotate,
                                    zrotate, rotate, ortho, frustum,
                                    perspective)
-from vispy.testing import run_tests_if_main
+from vispy.testing import run_tests_if_main, assert_equal
 
 
 def test_transforms():
diff --git a/vispy/util/tests/test_vispy.py b/vispy/util/tests/test_vispy.py
--- a/vispy/util/tests/test_vispy.py
+++ b/vispy/util/tests/test_vispy.py
@@ -5,10 +5,9 @@
 including configuration options.
 """
 
-from nose.tools import assert_raises, assert_equal, assert_not_equal
-
 import vispy.app
-from vispy.testing import requires_application, run_tests_if_main
+from vispy.testing import (requires_application, run_tests_if_main,
+                           assert_raises, assert_equal, assert_not_equal)
 
 
 @requires_application('pyside')
diff --git a/vispy/visuals/shaders/tests/test_function.py b/vispy/visuals/shaders/tests/test_function.py
--- a/vispy/visuals/shaders/tests/test_function.py
+++ b/vispy/visuals/shaders/tests/test_function.py
@@ -7,9 +7,8 @@
 # Users normally don't need these, but I want to test them
 from vispy.visuals.shaders.function import FunctionCall, TextExpression
 
-from nose.tools import assert_raises, assert_equal, assert_not_equal  # noqa
 from vispy.testing import (assert_in, assert_not_in, assert_is,
-                           run_tests_if_main)
+                           run_tests_if_main, assert_raises, assert_equal)
 
 
 ## Define some snippets