In March, VIA’s Ashley DaSilva, Team Leader, Product Development, was invited to lead a workshop on unit testing for McGill’s Computer Science Graduate Society. The workshop was part of their seminar series: CS Tools and Tricks, which introduces graduate students to topics they may not otherwise explore in depth in their academic programs. Ashley discussed the critical importance of software testing, why developers should embrace unit testing, and when and how to use mocking. She shares her experience with unit testing and a recap of her workshop below.
Learning Never Goes Out of Style
I first learned the importance of software testing in the early days of my theoretical physics Phd program. Back then, most of my coding was limited to scripting. I wrote scripts to model physical systems, analyze data, and visualize results. Over time, I started writing modules to be reused by myself or my colleagues for different projects. The first time I attempted to refactor one of these modules, it broke in unexpected ways, and I spent days tracking down and resolving all the issues.
I’ve grown significantly as a developer since those days, and now lead a product development team at VIA, focused specifically on the Trusted Analytics Chain™ (TAC™). Every day, my team and I build, test, and deploy Docker containers with microservices. This includes Airflow and RabbitMQ for scheduling tasks, Redis as a cache storage, and BigchainDB to host a blockchain. Software testing is critical to each stage of product development, and something we constantly work to improve.
Unit testing is the foundation of VIA’s software testing process, and an essential skill for all of our developers. For example, TAC™ contains several components that all need to communicate with each other. We maintain a list of python scripts that are authorized to run on the system. Components of TAC™ must download this list to verify the checksum of the scripts. If we did not use mocking for the content of the list in the unit tests of these verification functions, then every time the list was updated, the tests would all have to be updated to account for the change. With mocking, we are free to update the list of scripts without affecting the status of the unit tests.
Ready, Set, Resilience!
Clean Architectures in Python by Leonardo Giordani is a great resource for learning more about unit testing and test-driven development. During my workshop at McGill, I presented examples of unit testing and mocking and shared a few exercises for the students to practice on their own. Some of these exercises came from the github repository associated with Giordani’s book. I’ve included some other examples below:
First, let’s look at a snippet of code. The code below shows a DataAnalyzer class. It has a method, get_data, which is a placeholder for however one would want to retrieve the data from an external resource (e.g., a database or an http request). It also has a method,
analyze_data which performs the sum of the items in a list:
class DataAnalyzer: def get_data(self): # Gets data from an external resource pass def analyze_data(self): data = self.get_data() result = sum(data) return result
The code below shows one example of a unit test that uses mocking of the get_data method:
from unittest import mock from calc.analyzer import DataAnalyzer def test_analyzer(): analyzer = DataAnalyzer() with mock.patch("calc.analyzer.DataAnalyzer.get_data", return_value=[1.0, 2.1, 3.5]): result = analyzer.analyze_data() assert result == 6.6
In this example, the unittest.mock.patch will be applied to the method specified as its first argument and return the assigned return value every time that method is called from inside the scope of the patch. A sample list is assigned to the return value of the patch of the get_data method. This list should have the same format as the expected output of the get_data method, which in this case is a list of floating point numbers. Finally, the result of the analyze_data method is checked that it matches the expected value.
Mocking can also be used to check how you handle exceptions:
import pytest from unittest import mock from calc.analyzer import DataAnalyzer def test_analyzer_connection_error(): analyzer = DataAnalyzer() with mock.patch("calc.analyzer.DataAnalyzer.get_data", side_effect=ConnectionError("Could not connect.")): with pytest.raises(ConnectionError): analyzer.analyze_data()
In this example, instead of specifying a return value, there is a side effect. When the specified method is called, the side effect will be executed. In this case, a ConnectionError is raised by the get_data method. Using a side effect is particularly applicable when you have logic in your code that catches and recovers from errors.
I enjoyed leading a thoughtful discussion on unit testing, and fielded some great questions from the students. One that stood out to me was:
“How can developers make sure that their mocks don’t get out of date?”
This is a really important and sometimes tricky topic! At VIA, we know that unit testing is only the first step of software testing. We also use other types of tests, like integration tests or end-to-end tests help identify problems in mocking before our software reaches users. And isolating and resolving problems at that stage is key to setting ourselves up for a successful integration.
Mocking allows the freedom to isolate one particular part of your code and focus your unit tests on that functionality. Ideally, the expected inputs and outputs of the component being mocked are not going to change. This is typically true for external modules that you will use, at least within a particular major release of the software. However, if you know a software update will cause a change to your internal code base, it is your responsibility to recognize and communicate how that will affect your teammates. That’s why VIA believes that, in addition to testing, strong team communication and values like Learning Never Goes out of Style, Ready, Set, Resilience!, and Stay Curious are what help us develop and deliver the best iterations of our software to our users.