Index: sasmodels/kernelpy.py
===================================================================
--- sasmodels/kernelpy.py	(revision 108e70e22154db937a6d46f66be6dd70408cce6d)
+++ sasmodels/kernelpy.py	(revision 12eec1eafef77dae90813c12e016e43de8ffe1c4)
@@ -37,5 +37,5 @@
         self.info = model_info
         self.dtype = np.dtype('d')
-        logger.info("load python model " + self.info.name)
+        logger.info("make python model " + self.info.name)
 
     def make_kernel(self, q_vectors):
Index: sasmodels/model_test.py
===================================================================
--- sasmodels/model_test.py	(revision 012cd342ba6d27483147e3401f01fb7b93ea589c)
+++ sasmodels/model_test.py	(revision 12eec1eafef77dae90813c12e016e43de8ffe1c4)
@@ -47,4 +47,5 @@
 import sys
 import unittest
+import traceback
 
 try:
@@ -74,5 +75,4 @@
 # pylint: enable=unused-import
 
-
 def make_suite(loaders, models):
     # type: (List[str], List[str]) -> unittest.TestSuite
@@ -86,5 +86,4 @@
     *models* is the list of models to test, or *["all"]* to test all models.
     """
-    ModelTestCase = _hide_model_case_from_nose()
     suite = unittest.TestSuite()
 
@@ -95,70 +94,74 @@
         skip = []
     for model_name in models:
-        if model_name in skip:
-            continue
-        model_info = load_model_info(model_name)
-
-        #print('------')
-        #print('found tests in', model_name)
-        #print('------')
-
-        # if ispy then use the dll loader to call pykernel
-        # don't try to call cl kernel since it will not be
-        # available in some environmentes.
-        is_py = callable(model_info.Iq)
-
-        # Some OpenCL drivers seem to be flaky, and are not producing the
-        # expected result.  Since we don't have known test values yet for
-        # all of our models, we are instead going to compare the results
-        # for the 'smoke test' (that is, evaluation at q=0.1 for the default
-        # parameters just to see that the model runs to completion) between
-        # the OpenCL and the DLL.  To do this, we define a 'stash' which is
-        # shared between OpenCL and DLL tests.  This is just a list.  If the
-        # list is empty (which it will be when DLL runs, if the DLL runs
-        # first), then the results are appended to the list.  If the list
-        # is not empty (which it will be when OpenCL runs second), the results
-        # are compared to the results stored in the first element of the list.
-        # This is a horrible stateful hack which only makes sense because the
-        # test suite is thrown away after being run once.
-        stash = []
-
-        if is_py:  # kernel implemented in python
-            test_name = "%s-python"%model_name
-            test_method_name = "test_%s_python" % model_info.id
+        if model_name not in skip:
+            model_info = load_model_info(model_name)
+            _add_model_to_suite(loaders, suite, model_info)
+
+    return suite
+
+def _add_model_to_suite(loaders, suite, model_info):
+    ModelTestCase = _hide_model_case_from_nose()
+
+    #print('------')
+    #print('found tests in', model_name)
+    #print('------')
+
+    # if ispy then use the dll loader to call pykernel
+    # don't try to call cl kernel since it will not be
+    # available in some environmentes.
+    is_py = callable(model_info.Iq)
+
+    # Some OpenCL drivers seem to be flaky, and are not producing the
+    # expected result.  Since we don't have known test values yet for
+    # all of our models, we are instead going to compare the results
+    # for the 'smoke test' (that is, evaluation at q=0.1 for the default
+    # parameters just to see that the model runs to completion) between
+    # the OpenCL and the DLL.  To do this, we define a 'stash' which is
+    # shared between OpenCL and DLL tests.  This is just a list.  If the
+    # list is empty (which it will be when DLL runs, if the DLL runs
+    # first), then the results are appended to the list.  If the list
+    # is not empty (which it will be when OpenCL runs second), the results
+    # are compared to the results stored in the first element of the list.
+    # This is a horrible stateful hack which only makes sense because the
+    # test suite is thrown away after being run once.
+    stash = []
+
+    if is_py:  # kernel implemented in python
+        test_name = "%s-python"%model_info.name
+        test_method_name = "test_%s_python" % model_info.id
+        test = ModelTestCase(test_name, model_info,
+                                test_method_name,
+                                platform="dll",  # so that
+                                dtype="double",
+                                stash=stash)
+        suite.addTest(test)
+    else:   # kernel implemented in C
+
+        # test using dll if desired
+        if 'dll' in loaders or not use_opencl():
+            test_name = "%s-dll"%model_info.name
+            test_method_name = "test_%s_dll" % model_info.id
             test = ModelTestCase(test_name, model_info,
-                                 test_method_name,
-                                 platform="dll",  # so that
-                                 dtype="double",
-                                 stash=stash)
+                                    test_method_name,
+                                    platform="dll",
+                                    dtype="double",
+                                    stash=stash)
             suite.addTest(test)
-        else:   # kernel implemented in C
-
-            # test using dll if desired
-            if 'dll' in loaders or not use_opencl():
-                test_name = "%s-dll"%model_name
-                test_method_name = "test_%s_dll" % model_info.id
-                test = ModelTestCase(test_name, model_info,
-                                     test_method_name,
-                                     platform="dll",
-                                     dtype="double",
-                                     stash=stash)
-                suite.addTest(test)
-
-            # test using opencl if desired and available
-            if 'opencl' in loaders and use_opencl():
-                test_name = "%s-opencl"%model_name
-                test_method_name = "test_%s_opencl" % model_info.id
-                # Using dtype=None so that the models that are only
-                # correct for double precision are not tested using
-                # single precision.  The choice is determined by the
-                # presence of *single=False* in the model file.
-                test = ModelTestCase(test_name, model_info,
-                                     test_method_name,
-                                     platform="ocl", dtype=None,
-                                     stash=stash)
-                #print("defining", test_name)
-                suite.addTest(test)
-
-    return suite
+
+        # test using opencl if desired and available
+        if 'opencl' in loaders and use_opencl():
+            test_name = "%s-opencl"%model_info.name
+            test_method_name = "test_%s_opencl" % model_info.id
+            # Using dtype=None so that the models that are only
+            # correct for double precision are not tested using
+            # single precision.  The choice is determined by the
+            # presence of *single=False* in the model file.
+            test = ModelTestCase(test_name, model_info,
+                                    test_method_name,
+                                    platform="ocl", dtype=None,
+                                    stash=stash)
+            #print("defining", test_name)
+            suite.addTest(test)
+
 
 def _hide_model_case_from_nose():
@@ -348,11 +351,23 @@
     return abs(target-actual)/shift < 1.5*10**-digits
 
-def run_one(model):
-    # type: (str) -> str
-    """
-    Run the tests for a single model, printing the results to stdout.
-
-    *model* can by a python file, which is handy for checking user defined
-    plugin models.
+# CRUFT: old interface; should be deprecated and removed
+def run_one(model_name):
+    # msg = "use check_model(model_info) rather than run_one(model_name)"
+    # warnings.warn(msg, category=DeprecationWarning, stacklevel=2)
+    try:
+        model_info = load_model_info(model_name)
+    except Exception:
+        output = traceback.format_exc()
+        return output
+
+    success, output = check_model(model_info)
+    return output
+
+def check_model(model_info):
+    # type: (ModelInfo) -> str
+    """
+    Run the tests for a single model, capturing the output.
+
+    Returns success status and the output string.
     """
     # Note that running main() directly did not work from within the
@@ -369,11 +384,6 @@
     # Build a test suite containing just the model
     loaders = ['opencl'] if use_opencl() else ['dll']
-    models = [model]
-    try:
-        suite = make_suite(loaders, models)
-    except Exception:
-        import traceback
-        stream.writeln(traceback.format_exc())
-        return
+    suite = unittest.TestSuite()
+    _add_model_to_suite(loaders, suite, model_info)
 
     # Warn if there are no user defined tests.
@@ -390,5 +400,5 @@
     for test in suite:
         if not test.info.tests:
-            stream.writeln("Note: %s has no user defined tests."%model)
+            stream.writeln("Note: %s has no user defined tests."%model_info.name)
         break
     else:
@@ -406,5 +416,5 @@
     output = stream.getvalue()
     stream.close()
-    return output
+    return result.wasSuccessful(), output
 
 
Index: sasmodels/sasview_model.py
===================================================================
--- sasmodels/sasview_model.py	(revision d533590d9857d7aef962616cfdaa6ecfc2e6751d)
+++ sasmodels/sasview_model.py	(revision 12eec1eafef77dae90813c12e016e43de8ffe1c4)
@@ -786,4 +786,14 @@
             return value, [value], [1.0]
 
+    @classmethod
+    def runTests(cls):
+        """
+        Run any tests built into the model and captures the test output.
+
+        Returns success flag and output
+        """
+        from .model_test import check_model
+        return check_model(cls._model_info)
+
 def test_cylinder():
     # type: () -> float
