New to a Language? Write Some Tests.

Nick Nathan
unified-engineering
5 min readApr 13, 2020
Photo by Iker Urteaga on Unsplash

As an individual contributor at Unified I spend a fair amount of time writing tests to ensure that my code works as expected. I also suspect that for many developers this feels like an unnecessary obstacle to iterating quickly and releasing new features. While this is a hotly debated subject and there are legitimate arguments on both sides, the purpose of this article is not to make a case for or against testing as a general practice. Instead I argue that testing can be a powerful exercise for learning language fundamentals both for junior developers and for developers new to that language. Specifically, in order to write unit tests a developer often has to work with mocking libraries and pass around little bits of test state that force a more careful consideration of how objects are constructed and referenced.

In this post I’ll demonstrate how writing unit tests for even a very simple python program can force a developer to grapple with some core language principles and illustrate the reason for using functional best practices. Let’s take a look at some code.

This simple little program takes a dictionary object and passes it to three functions. While the first and last functions only reference the object, the second function, bar, modifies that object. Let’s run it and see what we get.

$ python3 main.py
foo
bar
foobar

Great, seems to work as expected! Now for the sake of this example let’s assume we want a test to check our program. Let’s also assume that we want to mock our foo function but we still want to verify that it was passed the correct arguments. In a real program this might be because it has a dependency on an external service like a database or API server etc.

As you can see we’ve nicely patched our foo method. You’ll notice that our first assert verifies that the actual foo method from the main package is the same object as our mock_foo method declared in our test. Next we run our main method which will call our mock and then we assert that the mock was called with the correct params. What do we get?

$ python3 test_main.py
[ ... ]
AssertionError: Expected call: foo({'f': 'foo', 'b': 'bar'})
Actual call: foo({'f': 'foo', 'b': 'bar', 'fb': 'foobar'})
— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —
Ran 1 test in 0.004s
FAILED (failures=1)

Huh?

For some reason it appears that even though we didn’t modify our object until after mock_foo was called, the assert function received the modified version of the object. What’s going on here? It turns out that when the mock function object stores the arguments for each call it’s actually storing a pointer to the original arguments. Though bar is called after mock_foo, the mock still points to the original object and therefore the assertion references the modified version. This is a good example of how python passes a copy of a reference to an object. In other words, mock_foo was passed one pointer to the object while bar was passed a different pointer to the same object.

Now that we understand what’s going on how can we write a test to ensure that our assert function can check our object state before it’s modified? One approach might be to assert the arguments as soon as they’re passed to the mock_foo function and thus before they are modified downstream by bar. In order to do this we take advantage of the side_effect attribute on the mock object. If you set a mock’s side_effect attribute to a function then when the mock function is called the same arguments to the mock function will also be passed the side_effect function. Why is this useful? Because it enables us to write our own version of assert_called_once_with that runs as soon as the mock function is called. Let’s see how this would work by modifying our test as follows.

As you can see above we’ve defined our own custom_assert_called_once_with that returns a function which we assign to the side_effect attribute of our mock_foo method. When we run the main method mock_foo will be called and the arguments will also be passed to our custom assertion. Let’s run it and see what happens.

$ python3 test_main_updated.py
bar
foobar
.
— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —
Ran 1 test in 0.000s
OK

And voila! As we can see “foo” was never printed because our mock_foo was being called instead. We were able to successfully verify that the correct number of arguments were passed to our mock and that the state of those arguments was correct because bar had not yet modified them. So how does our custom assertion method work? In custom_assert_called_once_with we define two objects: a dictionary called expected_args and a function called assert_args. The function assert_args is returned from custom_assert_called_once_with but because we’ve created a closure that function has access to the expected_args object.

We’ve now solved the problem of testing our foo function however the solution was not exactly obvious and we had to write a fairly complex custom assertion method. There must be a better way. Perhaps we could rethink the way we wrote our original program? As written bar is not a “pure function” because it has a “side effect”. In other words it modifies state outside of itself. This not only makes the program more difficult to reason about but, as demonstrated in our test, it causes the program to behave in unpredictable ways. In order to adhere to the best principles of functional programming, side effects should really be remove all together. This time let’s rewrite our program instead of our test.

And rerun our original test …

$ python3 test_main.py
bar
foobar
.
— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —
Ran 1 test in 0.001s
OK

It worked!

By converting our bar method to a pure function we’re able to remove all side effects and eliminate the need for complicated testing techniques. So what’s the main takeaway here? While writing tests can be a bit of a pain they can often reveal interesting things about the language itself and can encourage the use of best practices. As this example demonstrates, writing even a basic test will push developers, especially the more junior ones, to think more deeply about how to design solutions and help them utilize functional best practices that ultimately improve their code and prevent bugs.

I hope you all enjoyed this post and found a fresh perspective on testing! If you’re looking to build your skills and become a better developer then be sure to checkout Unified at https://unified.com/about/careers-and-culture.

--

--

Nick Nathan
unified-engineering

Building apps and technical infrastructure for startups and growing businesses.