Popular node unit testing frameworks emphasize both spying and mocking in unittests. Because so much IO regularly occurs in node projects, planning a strategy to mock on IO in unittests should be a high priority. When testing it's usually pretty clear what methods need to be spied on (any that make IO requests :) But if code isn't architected to be testable from the beginning, then spies, mocks and patches end up being applied in roundabout ways. Lately I've been seeing a common mistake made with spying on exported module objects. This blog posts assumes understanding of testing spies.
What are some ways to spy on exported module methods?
Assume we have a module resources.js. All of its methods are being exported for unittesting.
buildRequest is easily tested because it has no IO calls. The difficulty arises when we try to test getData and makeRequest. Our goal for this blog is to write a test that asserts getData calls makeRequest a single time with the correct arguments. Since makeRequest performs IO, and IO is a no-no for our unittests, we have to mock it in some way. Our fist attempt at doing so is:
In our test resources.js is required and then makeRequest is mocked and spied on, so the test can make assertions on it, and no IO is performed. The output of running the above test is provided, which results in an error. This is the initial attempt that I've been seeing very frequently. The issue is the spy is being created on the exported objects property and NOT the makeRequest defined in and used in resources.js.
To illustrate this; if resources.js were to use the object it is exporting in its calls, then the spy would be created and used as expected!!!
The above sample modifies resources.js to use the object it is exporting, which is the same object the spy is being applied to. The output of running the test in test.spec.js against the above resources.js file is shown. The test PASSES! (In the near future I plan on having a blog that shows, where, why, how objects are exported/required referencing node core code)
While the above works, I personally don't think it is very clear, (and haven't really seen modules that use module.exports in its function implementations. I also think it is dangerous to reference module.exports internally to a module because, clients of that module can mutate it!!!!
A similar way to mock makeRequest is to export a reference to an object used internally.
The above code, defines a utils object in resources.js and exports a reference to it. Because it is a reference, the test can be updated to spy on resources.utils.makeRequest and the utils object in resources.js will be mutated.
I think this way is cleaner than using module.exports directly, but is still susceptible to being mutated by a client!
Dependency injection is a strong tool for creating node code that can be easily tested; by providing a clean way to mock IO dependencies. It requires that the caller provides (injects) a functions dependencies. In this case getData depends on functions that perform IO, makeRequest. Refactoring it would require the caller of getData to provide a makeRequest function. This would seamlessly allow a test to provide a mocked makeRequest method (that doesn't make IO calls), and the actual code to provide a different makeRequest method (which does make IO calls).
getData now requires that a requestor method is provided by the client. The test code is free to provide a spy as the requestor, while the production clients will be required to provide makeRequest
getData(resource, resources.makeRequest, callback);
For the above example, getData only has a single dependency, while frequently in real world code methods may have multiple IO dependencies. While functions can usually be decomposed and refactored to achieve making a single call or two, doing so after code is already in production is dangerous. Because of how easy it is to create extremely nested node.js code it is very beneficial to design testable code from the start. A powerful tool to do this is dependency injection, something I plan on writing a lot more about very soon.
Keep testing, happy noding!!!
Showing posts with label test suite. Show all posts
Showing posts with label test suite. Show all posts
Sunday, November 29, 2015
Friday, September 21, 2012
Introduction to Unit Testing Using Python Unittest
Unit Testing is an extremely powerful tool. It directly helps to ensure the 3 aspects of good software: Verifiability, Maintainability, and Extensibility. Unit testing is as much a process of software design as it is a tool. There are many great tutorials on HOW to use python unittest, and of course python documentation is an excellent resource. I am going to focus on WHY to test your software. Below are a couple short unit testing examples using python's built in unittest package.
Verfiability
How does one verify that their program works? With unit testing we can create specific functions or groups of functions to target our code. For example:
We can easily create a test for this using python's built in unittest module. Tests are created by subclassing unittest.TestClass
On python 2.7+ running python -m unittest discover in our package will automatically search for all test.py files and run the TestClasses. There are a couple of things to note in the above example. All test classes must subclass unittest.TestClass. The focal point of any test is its assertions. The assertions are what dictate whether a test passes or fails. Although this is a contrived example it displays how easy it is to isolate our methods and control exactly what inputs they receive! If we were to create one test method (or more) for every method in our project we would quickly grow a test suite. When making ANY changes to our code it becomes trivial to run through every single function in our project and verify that nothing has broken! Imagine if we had a web app and every time we added a feature we would have to run through EVERY possible page/action!? It could take a long time doing it manually.
Maintainability
Bugs are a part of software development. It is extremely important to minimize bugs but when they do happen it is important to create fixes very quickly. Because bugs will occur in code it is important to have a process set up that helps to isolate the bug so that it is easy to reproduce, easy to correct and easy to verify the bug has been fixed. Unittesting helps to do all of these. Suppose the sum_numbers function is at the heart of a website. It gets all sorts of user input data, and occasionally some faulty data slips through. If a string is passed as one of the parameters it will result in a TypeError!! It is trivial to isolate and reproduce this bug. I guess we need a little thought about what should be returned if there is an invalid input. For this example lets return None. We then can create another test method like:
Running the above code reproduces the string error. Since we haven't fixed our code yet this test will fail. We can then change our sum_numbers method to handle a type error:
Running our test again will result in two passing tests. We successfully isolated the bug, reproduced the bug and verified the bug has been fixed!! We now also have a test trail assuring us the big has been addressed. Pretty cool.
Extensibility
With tests it becomes very easy to help an app grow. A test suite provides a safety net for an application. We can programatically run through every function of an app in a short amount of time. This could take hours to do manually. Some test suites can take hours to run, it wouldn't even be feasible to manually test a large codebase!! As long as we keep designing our apps in a modular unit based way we can easily add functions and tests for those individual functions. Another aspect of unittesting is how easy it is to refactor code. Suppose we had thought it was a good idea at the time to write our original function like:
Assuming we had the same test method as before:
This test is focused on the output of our function. It is assuring us the output is as expected. This allows us to easily change what is happening in our function and still have the test acting as a safety net. We can rewrite (refactor) the internals of our methods and guarentee they are still functioning in the way we originally tested them! Our method would pass the test because it is performing the action that we want it to. We could change the method to remove the list and sum function
We cleaned up our function and ensures that it functions in the way we designed it to!!
Testing is a powerful tool that should be very heavily considered. It helps verify our functions are working the way we intended them, help us easily maintain our applications and help us extend our applications. Correct aplication design will help us isolate our problems. Testing can take a significant amount of time, but the benefits it offers far outweigh any downsides.
Verfiability
How does one verify that their program works? With unit testing we can create specific functions or groups of functions to target our code. For example:
def sum_numbers(num_one, num_two): """return an integer, the sum of two numbers""" return num_one + num_two
We can easily create a test for this using python's built in unittest module. Tests are created by subclassing unittest.TestClass
from mymodule.functions import sum_numbers class TestFunctions(unittest.TestClass): def test_add_numbers_success(self): self.assertEqual(sum_numbers(2, 2), 4)
On python 2.7+ running python -m unittest discover in our package will automatically search for all test.py files and run the TestClasses. There are a couple of things to note in the above example. All test classes must subclass unittest.TestClass. The focal point of any test is its assertions. The assertions are what dictate whether a test passes or fails. Although this is a contrived example it displays how easy it is to isolate our methods and control exactly what inputs they receive! If we were to create one test method (or more) for every method in our project we would quickly grow a test suite. When making ANY changes to our code it becomes trivial to run through every single function in our project and verify that nothing has broken! Imagine if we had a web app and every time we added a feature we would have to run through EVERY possible page/action!? It could take a long time doing it manually.
Maintainability
Bugs are a part of software development. It is extremely important to minimize bugs but when they do happen it is important to create fixes very quickly. Because bugs will occur in code it is important to have a process set up that helps to isolate the bug so that it is easy to reproduce, easy to correct and easy to verify the bug has been fixed. Unittesting helps to do all of these. Suppose the sum_numbers function is at the heart of a website. It gets all sorts of user input data, and occasionally some faulty data slips through. If a string is passed as one of the parameters it will result in a TypeError!! It is trivial to isolate and reproduce this bug. I guess we need a little thought about what should be returned if there is an invalid input. For this example lets return None. We then can create another test method like:
class TestFunctions(unittest.TestClass): def test_add_numbers_success(self): self.assertEqual(sum_numbers(2, 2), 4)
def test_add_numbers_string_bug(self):
self.assertEqual(sum_numbers('a', 2), None)
Running the above code reproduces the string error. Since we haven't fixed our code yet this test will fail. We can then change our sum_numbers method to handle a type error:
def sum_numbers(num_one, num_two): """return an integer, the sum of two numbers, can't trust user input"""
try: return int(num_one) + int(num_two)
except ValueError:
return None
Running our test again will result in two passing tests. We successfully isolated the bug, reproduced the bug and verified the bug has been fixed!! We now also have a test trail assuring us the big has been addressed. Pretty cool.
Extensibility
With tests it becomes very easy to help an app grow. A test suite provides a safety net for an application. We can programatically run through every function of an app in a short amount of time. This could take hours to do manually. Some test suites can take hours to run, it wouldn't even be feasible to manually test a large codebase!! As long as we keep designing our apps in a modular unit based way we can easily add functions and tests for those individual functions. Another aspect of unittesting is how easy it is to refactor code. Suppose we had thought it was a good idea at the time to write our original function like:
def sum_numbers(num_one, num_two): """return an integer, the sum of two numbers""" return sum([num_one, num_two])
Assuming we had the same test method as before:
def test_add_numbers_success(self): self.assertEqual(sum_numbers(2, 2), 4)
This test is focused on the output of our function. It is assuring us the output is as expected. This allows us to easily change what is happening in our function and still have the test acting as a safety net. We can rewrite (refactor) the internals of our methods and guarentee they are still functioning in the way we originally tested them! Our method would pass the test because it is performing the action that we want it to. We could change the method to remove the list and sum function
def sum_numbers(num_one, num_two): """return an integer, the sum of two numbers""" return num_one + num_two
We cleaned up our function and ensures that it functions in the way we designed it to!!
Testing is a powerful tool that should be very heavily considered. It helps verify our functions are working the way we intended them, help us easily maintain our applications and help us extend our applications. Correct aplication design will help us isolate our problems. Testing can take a significant amount of time, but the benefits it offers far outweigh any downsides.
Subscribe to:
Posts (Atom)