7

I fully realize that the order of unit tests should not matter. But these unit tests are as much for instructional use as for actual unit testing, so I would like the test output to match up with the test case source code.

I see that there is a way to set the sort order by setting the sortTestMethodsUsing attribute on the test loader. The default is a simple cmp() call to lexically compare names. So I tried writing a cmp-like function that would take two names, find their declaration line numbers and them return the cmp()-equivalent of them:

import unittest

class TestCaseB(unittest.TestCase):
    def test(self):
        print("running test case B")

class TestCaseA(unittest.TestCase):
    def test(self):
        print("running test case A")

import inspect
def get_decl_line_no(cls_name):
    cls = globals()[cls_name]
    return inspect.getsourcelines(cls)[1]

def sgn(x):
    return -1 if x < 0 else 1 if x > 0 else 0

def cmp_class_names_by_decl_order(cls_a, cls_b):
    a = get_decl_line_no(cls_a)
    b = get_decl_line_no(cls_b)
    return sgn(a - b)

unittest.defaultTestLoader.sortTestMethodsUsing = cmp_class_names_by_decl_order
unittest.main()

When I run this, I get this output:

running test case A
.running test case B
.
----------------------------------------------------------------------
Ran 2 tests in 0.000s

OK

indicating that the test cases are not running in the declaration order.

My sort function is just not being called, so I suspect that main() is building a new test loader, which is wiping out my sort function.

PaulMcG
  • 62,419
  • 16
  • 94
  • 130
  • Can you provide a [mcve] demonstrating your problem? The code you've posted doesn't include any tests, so it's not going to need to sort any tests. – user2357112 Sep 24 '18 at 02:54
  • (If this *is* the code you ran, then there's your answer: it's not sorting anything because there's nothing to sort.) – user2357112 Sep 24 '18 at 02:55
  • BTW, I guess this is a Python2 project right? As there is no `cmp` in Python3. – Sraw Sep 24 '18 at 02:57
  • Actually it is a library module that tries to maintain compatibility with Py2.6 thru 3.latest. I forgot that Py3 no longer has `cmp()` - thanks for the catch. – PaulMcG Sep 24 '18 at 03:07
  • If you have more than one methods in a class, you will get an exception. And then you will know why. – Sraw Sep 24 '18 at 03:07
  • I just added a method to TestCaseA - no exception (I have removed the offending cmp() call, see edits). What insight was I supposed to gain from this? – PaulMcG Sep 24 '18 at 03:11
  • As @user2357112 has said, it is used to sort methods in **one** class. If you just have only one method, it won't be called at all. But if you have more than one methods, you should meet an exception telling you that `KeyError: 'xxx'` because in global scope there is no `xxx`, that's in the scope of a class. – Sraw Sep 24 '18 at 03:14
  • 1
    And [here](https://stackoverflow.com/a/36430378/5588279) is a related answer, I think it may help you if you really want to sort classes instead of methods. – Sraw Sep 24 '18 at 03:20
  • Most constructive comment so far, see my self-answer for a working solution. – PaulMcG Sep 24 '18 at 04:11
  • [this](https://stackoverflow.com/a/50283484/6243352) seems like a good approach. – ggorlen Jun 18 '20 at 05:32

3 Answers3

3

The solution is to create a TestSuite explicitly, instead of letting unittest.main() follow all its default test discovery and ordering behavior. Here's how I got it to work:

import unittest

class TestCaseB(unittest.TestCase):
    def runTest(self):
        print("running test case B")

class TestCaseA(unittest.TestCase):
    def runTest(self):
        print("running test case A")


import inspect
def get_decl_line_no(cls):
    return inspect.getsourcelines(cls)[1]

# get all test cases defined in this module
test_case_classes = list(filter(lambda c: c.__name__ in globals(), 
                                unittest.TestCase.__subclasses__()))

# sort them by decl line no
test_case_classes.sort(key=get_decl_line_no)

# make into a suite and run it
suite = unittest.TestSuite(cls() for cls in test_case_classes)
unittest.TextTestRunner().run(suite)

This gives the desired output:

running test case B
.running test case A
.
----------------------------------------------------------------------
Ran 2 tests in 0.000s

OK

It is important to note that the test method in each class must be named runTest.

PaulMcG
  • 62,419
  • 16
  • 94
  • 130
2

You can manually build a TestSuite where your TestCases and all tests inside them run by line number:

# Python 3.8.3
import unittest
import sys
import inspect


def isTestClass(x):
    return inspect.isclass(x) and issubclass(x, unittest.TestCase)


def isTestFunction(x):
    return inspect.isfunction(x) and x.__name__.startswith("test")


class TestB(unittest.TestCase):
    def test_B(self):
        print("Running test_B")
        self.assertEqual((2+2), 4)

    def test_A(self):
        print("Running test_A")
        self.assertEqual((2+2), 4)

    def setUpClass():
        print("TestB Class Setup")


class TestA(unittest.TestCase):
    def test_A(self):
        print("Running test_A")
        self.assertEqual((2+2), 4)

    def test_B(self):
        print("Running test_B")
        self.assertEqual((2+2), 4)

    def setUpClass():
        print("TestA Class Setup")


def suite():

    # get current module object
    module = sys.modules[__name__]

    # get all test className,class tuples in current module
    testClasses = [
        tup for tup in
        inspect.getmembers(module, isTestClass)
    ]

    # sort classes by line number
    testClasses.sort(key=lambda t: inspect.getsourcelines(t[1])[1])

    testSuite = unittest.TestSuite()

    for testClass in testClasses:
        # get list of testFunctionName,testFunction tuples in current class
        classTests = [
            tup for tup in
            inspect.getmembers(testClass[1], isTestFunction)
        ]

        # sort TestFunctions by line number
        classTests.sort(key=lambda t: inspect.getsourcelines(t[1])[1])

        # create TestCase instances and add to testSuite;
        for test in classTests:
            testSuite.addTest(testClass[1](test[0]))

    return testSuite


if __name__ == '__main__':

    runner = unittest.TextTestRunner()
    runner.run(suite())

Output:

TestB Class Setup
Running test_B
.Running test_A
.TestA Class Setup
Running test_A
.Running test_B
.
----------------------------------------------------------------------
Ran 4 tests in 0.000s

OK
ghost
  • 21
  • 3
1

As stated in the name, sortTestMethodsUsing is used to sort test methods. It is not used to sort classes. (It is not used to sort methods in different classes either; separate classes are handled separately.)

If you had two test methods in the same class, sortTestMethodsUsing would be used to determine their order. (At that point, you would get an exception because your function expects class names.)

user2357112
  • 260,549
  • 28
  • 431
  • 505
  • 1
    While this gives some insight into why the method I tried in my sample code is not going to do what I want, I'm not sure that qualifies as an "answer" to the actual question.. – PaulMcG Sep 24 '18 at 04:09