2

I'm trying to write unit tests in Python and struggle to find a descriptive way to do things. I have a JavaScript background and I use mocha which helps me be descriptive.

This is what I mean by "descriptive":

foo.js

exports.foo = function (type, isLogged, iOk) {
    if (type === undefined) throw new Error('a cannot be undefined');
    if (isLogged === undefined) throw new Error('b cannot be undefined');
    if (isLogged) {
        if (type === 'WRITER') {
            return isOk ? "writer" : -1;
        } else {
            return "something else"
        }
    }
}

foo.spec.js

describe('#foo()', function () {
    context('when type is undefined', function () {
      ...
    })
    context('when isLogged is undefined', function () {
      ...
    })
    context('when type is defined', function () {
        context('when isLogger is not defined', function () {
         ...
        })
        context('when isLogged is defined', function () {
            context('when type is not WRITER', function () {
             ...
            })
            context('when type is WRITER', function () {
                context('when isOk is true', function () {
                 ...
                })
            })
        }) 
    })
})

While when I write unit tests in Python, I end up with somthing like this:

foo.spec.py

class TestFoo:
    def test_when_type_is_undefined(self):
        ...
    def test_when_isLogged_is_undefined(self):
        ...
    # This test name is too long
    def test_when_type_is_defined_and_isLogged_is_undefined_and_type_is_writer_when_is_ok_is_true(self):
        ...

How to structure these test in a better way? What are the best practices regarding descriptive unit testing? Are there good examples of good unit tests?

Simon
  • 1,679
  • 4
  • 21
  • 37

3 Answers3

2

You can use pyspecs to have a more BDD-like syntax in your code.

Example:

from pyspecs import given, when, then, and_, the, this

with given.two_operands:
    a = 2
    b = 3

    with when.supplied_to_the_add_function:
        total = a + b

        with then.the_total_should_be_mathmatically_correct:
            the(total).should.equal(5)

        with and_.the_total_should_be_greater_than_either_operand:
            the(total).should.be_greater_than(a)
            the(total).should.be_greater_than(b)

    with when.supplied_to_the_subtract_function:
        difference = b - a

        with then.the_difference_should_be_mathmatically_correct:
            the(difference).should.equal(1)

Output

# run_pyspecs.py

  | • given two operands 
  |   • when supplied to the add function 
  |     • then the total should be mathmatically correct 
  |     • and the total should be greater than either operand 
  |   • when supplied to the subtract function 
  |     • then the difference should be mathmatically correct 

(ok) 6 passed (6 steps, 1 scenarios in 0.0002 seconds)
Sven
  • 2,839
  • 7
  • 33
  • 53
0

I don't see any problem with your unit test construction. Unit tests should be very descriptive so it is apparent what the problem is once a test fails. As a developer with little information, the testcase test_when_type_is_defined_and_isLogged_is_undefined_and_type_is_writer_when_is_ok_is_true tells me a lot about what went wrong and where to look.

You can make your tests even more descriptive by adding an error message in your assert statements, so that when something fails, you know exactly why. For example: "Expected writer to be ok but writer was None".

To me, the name of the file that the test is in, the name of the test case, and the assertion message should give a clear path to what code failed and why.

Alex
  • 1,432
  • 14
  • 26
  • While I mostly agree with you, if I try to comply with PEP8, the method name cannot exceed 100 characters. How to manage to do both? – Simon Nov 22 '16 at 14:43
  • 1
    I follow PEP8 almost religiously. However it is a [guideline](https://www.python.org/dev/peps/pep-0008/#a-foolish-consistency-is-the-hobgoblin-of-little-minds) and not a mandate. If you code makes more sense and is more readable to you this way, then that's the best way for you! – Alex Nov 22 '16 at 14:45
0

Having meaningful test method names is, of course, important but, when a test name becomes unpractically long and non-readable, you can always provide full test descriptions inside method docstrings.

Here are some sample tests from the requests library:

def test_cookielib_cookiejar_on_redirect(self, httpbin):
    """Tests resolve_redirect doesn't fail when merging cookies
    with non-RequestsCookieJar cookiejar.
    See GH #3579
    """
    cj = cookiejar_from_dict({'foo': 'bar'}, cookielib.CookieJar())
    s = requests.Session()
    # ...

def test_headers_on_session_with_None_are_not_sent(self, httpbin):
    """Do not send headers in Session.headers with None values."""
    ses = requests.Session()
    ses.headers['Accept-Encoding'] = None
    # ...

Note that you can see these docstrings on the console with the increased verbosity. Demo:

$ cat test_module.py
import unittest


class BasicTestSuite(unittest.TestCase):
    def test_one(self):
        self.assertEqual(1, 1)

    def test_two(self):
        """Extended description"""
        self.assertEqual(2, 2)

if __name__ == '__main__':
    unittest.main()

$ python -m unittest -v test_module
test_one (test_module.BasicTestSuite) ... ok
test_two (test_module.BasicTestSuite)
Extended description ... ok

----------------------------------------------------------------------
Ran 2 tests in 0.000s

OK
alecxe
  • 462,703
  • 120
  • 1,088
  • 1,195