Unit testing is an essential instrument in the toolbox of any serious software developer. However, it can sometimes be quite difficult to write a good unit test for a particular piece of code. Having difficulty testing their own or someone else’s code, developers often think that their struggles are caused by a lack of some fundamental testing knowledge or secret unit testing techniques. Show
In this unit testing tutorial, I intend to demonstrate that unit tests are quite easy; the real problems that complicate unit testing, and introduce expensive complexity, are a result of poorly-designed, untestable code. We will discuss what makes code hard to test, which anti-patterns and bad practices we should avoid to improve testability, and what other benefits we can achieve by writing testable code. We will see that writing unit tests and generating testable code is not just about making testing less troublesome, but about making the code itself more robust, and easier to maintain. What is Unit Testing?Essentially, a unit test is a method that instantiates a small portion of our application and verifies its behavior independently from other parts. A typical unit test contains 3 phases: First, it initializes a small piece of an application it wants to test (also known as the system under test, or SUT), then it applies some stimulus to the system under test (usually by calling a method on it), and finally, it observes the resulting behavior. If the observed behavior is consistent with the expectations, the unit test passes, otherwise, it fails, indicating that there is a problem somewhere in the system under test. These three unit test phases are also known as Arrange, Act and Assert, or simply AAA. A unit test can verify different behavioral aspects of the system under test, but most likely it will fall into one of the following two categories: state-based or interaction-based. Verifying that the system under test produces correct results, or that its resulting state is correct, is called state-based unit testing, while verifying that it properly invokes certain methods is called interaction-based unit testing. As a metaphor for proper software unit testing, imagine a mad scientist who wants to build some supernatural chimera, with frog legs, octopus tentacles, bird wings, and a dog’s head. (This metaphor is pretty close to what programmers actually do at work). How would that scientist make sure that every part (or unit) he picked actually works? Well, he can take, let’s say, a single frog’s leg, apply an electrical stimulus to it, and check for proper muscle contraction. What he is doing is essentially the same Arrange-Act-Assert steps of the unit test; the only difference is that, in this case, unit refers to a physical object, not to an abstract object we build our programs from.
An example of a simple unit test could look like this:
Unit Test vs. Integration TestAnother important thing to consider is the difference between unit testing and integration testing. The purpose of a unit test in software engineering is to verify the behavior of a relatively small piece of software, independently from other parts. Unit tests are narrow in scope, and allow us to cover all cases, ensuring that every single part works correctly. On the other hand, integration tests demonstrate that different parts of a system work together in the real-life environment. They validate complex scenarios (we can think of integration tests as a user performing some high-level operation within our system), and usually require external resources, like databases or web servers, to be present. Let’s go back to our mad scientist metaphor, and suppose that he has successfully combined all the parts of the chimera. He wants to perform an integration test of the resulting creature, making sure that it can, let’s say, walk on different types of terrain. First of all, the scientist must emulate an environment for the creature to walk on. Then, he throws the creature into that environment and pokes it with a stick, observing if it walks and moves as designed. After finishing a test, the mad scientist cleans up all the dirt, sand and rocks that are now scattered in his lovely laboratory. Notice the significant difference between unit and integration tests: A unit test verifies the behavior of small part of the application, isolated from the environment and other parts, and is quite easy to implement, while an integration test covers interactions between different components, in the close-to-real-life environment, and requires more effort, including additional setup and teardown phases. A reasonable combination of unit and integration tests ensures that every single unit works correctly, independently from others, and that all these units play nicely when integrated, giving us a high level of confidence that the whole system works as expected. However, we must remember to always identify what kind of test we are implementing: a unit or an integration test. The difference can sometimes be deceiving. If we think we are writing a unit test to verify some subtle edge case in a business logic class, and realize that it requires external resources like web services or databases to be present, something is not right — essentially, we are using a sledgehammer to crack a nut. And that means bad design. What Makes a Good Unit Test?Before diving into the main part of this tutorial and writing unit tests, let’s quickly discuss the properties of a good unit test. Unit testing principles demand that a good test is:
That’s it — there are no secrets to writing unit tests. However, there are some techniques that allow us to write testable code. Testable and Untestable CodeSome code is written in such a way that it is hard, or even impossible, to write a good unit test for it. So, what makes code hard to test? Let’s review some anti-patterns, code smells, and bad practices that we should avoid when writing testable code. Poisoning the Codebase with Non-Deterministic FactorsLet’s start with a simple example. Imagine that we are writing a program for a smart home microcontroller, and one of the requirements is to automatically turn on the light in the backyard if some motion is detected there during the evening or at night. We have started from the bottom up by implementing a method that returns a string representation of the approximate time of day (“Night”, “Morning”, “Afternoon” or “Evening”):
Essentially, this method reads the current system time and returns a result based on that value. So, what’s wrong with this code? If we think about it from the unit testing perspective, we’ll see that it is not possible to write a proper state-based unit test for this method. Such non-deterministic behavior makes it impossible to test the internal logic of the
Tests like this would violate a lot of the rules discussed earlier. It would be expensive to write (because of the non-trivial setup and teardown logic), unreliable (it may fail even if there are no bugs in the system under test, due to system permission issues, for example), and not guaranteed to run fast. And, finally, this test would not actually be a unit test — it would be something between a unit and integration test, because it pretends to test a simple edge case but requires an environment to be set up in a particular way. The result is not worth the effort, huh? Turns out that all these testability problems are caused by the low-quality
After reviewing the API, let’s finally fix it! Fortunately, this is much easier than discussing all of its flaws — we just need to break the tightly coupled concerns. Fixing the API: Introducing a Method ArgumentThe most obvious and easy way of fixing the API is by introducing a method argument:
Now the method requires the caller to provide a
Notice that this simple refactor also solved all the API issues discussed earlier (tight coupling, SRP violation, unclear and hard to understand API) by introducing a clear seam between what data should be processed and how it should be done. Excellent — the method is testable, but how about its clients? Now it is the caller’s responsibility to provide date and time to the Fixing the Client API: Dependency InjectionSay we continue working on the smart home system, and implement the following
client of the
Ouch! We have the same kind of hidden Inversion of Control is a simple, but extremely useful, technique for decoupling code, and for unit testing in particular. (After all, keeping things loosely coupled is essential for being able to analyze them independently from each other.) The key point of IoC is to separate decision-making code (when to do something) from action code (what to do when something happens). This technique increases flexibility, makes our code more modular, and reduces coupling between components. Inversion of Control can be implemented in a number of ways; let’s have a look at one particular example —
Dependency Injection using a constructor — and how it can help in building a testable First, let’s create an
Then, make
Now we can see why Inversion of Control is so called: the
control of what mechanism to use for reading date and time was inverted, and now belongs to the client of Why is this significant for unit testing? It means that different A fake implementation of
With the help of this class, it is possible to isolate
Great! A test like this was not possible before refactoring. Now that we’ve eliminated non-deterministic factors and verified the state-based scenario, do you think Poisoning the Codebase with Side EffectsDespite the fact that we solved the problems caused by the non-deterministic hidden input, and we were able to test certain functionality, the code (or, at least, some of it) is still untestable! Let’s review the following part of the
As we can see, To fully unit test the Indeed, let’s suppose that the motion sensor, backyard lantern, and smart home microcontroller are connected into an Internet of Things network and communicate using some wireless protocol. In this case, a unit test can make an attempt to receive and analyze that network traffic. Or, if the hardware components are connected with a wire, the unit test can check whether the voltage was applied to the appropriate electrical circuit. Or, after all, it can check that the light actually turned on or off using an additional light sensor. As we can see, unit testing side-effecting methods could be as hard as unit testing non-deterministic ones, and may even be impossible. Any attempt will lead to problems similar to those we’ve already seen. The resulting test will be hard to implement, unreliable, potentially slow, and not-really-unit. And, after all that, the flashing of the light every time we run the test suite will eventually drive us crazy! Again, all these testability problems are caused by the bad API, not the developer’s ability to write unit tests. No matter how exactly light control is implemented, the
The solution of both testability and low-quality API issues is, not surprisingly, to break tightly coupled components from each other. As with the previous example, employing Dependency Injection would solve these issues; just add an Fixing the API: Higher-Order FunctionsThis approach is an option in any object-oriented language that supports first-class functions. Let’s take
advantage of C#’s functional features and make the
This is a more functional-flavored solution than the classic object-oriented Dependency Injection approach we’ve seen before;
however, it lets us achieve the same result with less code, and more expressiveness, than Dependency Injection. It is no longer necessary to implement a class that conforms to an interface in order to supply Now, to perform an interaction-based unit test of the resulting method, we can pass easily verifiable fake actions into it:
Finally, we have made the Now, in order to achieve full unit test coverage, we can simply implement a bunch of similar-looking tests to validate all possible cases — not a big deal since unit tests are now quite easy to implement. Impurity and TestabilityUncontrolled non-determinism and side effects are similar in their destructive effects on the codebase. When used carelessly, they lead to deceptive, hard to understand and maintain, tightly coupled, non-reusable, and untestable code. On the other hand, methods that are both deterministic and side-effect-free are much easier to test, reason about, and reuse to build larger programs. In terms of functional programming, such methods are called pure functions. We’ll rarely have a problem unit testing a pure function; all we have to do is to pass some arguments and check the result for correctness. What really makes code untestable is hard-coded, impure factors that cannot be replaced, overridden, or abstracted away in some other way. Impurity
is toxic: if method However, impurity is inevitable; any real-life application must, at some point, read and manipulate state by interacting with the environment, databases, configuration files, web services, or other external systems. So instead of aiming to eliminate impurity altogether, it’s a good idea to limit these factors, avoid letting them poison your codebase, and break hard-coded dependencies as much as possible, in order to be able to analyze and unit test things independently. Common Warning Signs of Hard to Test CodeTrouble writing tests? The problem's not in your test suite. It's in your code. Finally, let’s review some common warning signs indicating that our code might be difficult to test. Static Properties and FieldsStatic properties and fields or, simply put, global state, can complicate code comprehension and testability, by hiding the information required for a method to get its job done, by introducing non-determinism, or by promoting extensive usage of side effects. Functions that read or modify mutable global state are inherently impure. For example, it is hard to reason about the following code, which depends on a globally accessible property:
What if the On the other hand, immutable and deterministic global state is totally OK. In fact, there’s a more familiar name for this — a constant. Constant values like
SingletonsEssentially, the Singleton pattern is just another form of the global state. Singletons promote obscure APIs that lie about real dependencies and introduce unnecessarily tight coupling between components. They also violate the Single Responsibility Principle because, in addition to their primary duties, they control their own initialization and lifecycle. Singletons can easily make unit tests order-dependent because they carry state around for the lifetime of the whole application or unit test suite. Have a look at the following example:
In the example above, if a test for the cache-hit scenario runs first, it will add a new user to the cache, so a subsequent test of the cache-miss scenario may fail because it assumes that the cache is empty. To overcome this, we’ll have to write additional teardown code to clean the Using Singletons is a bad practice that can (and should) be avoided in most cases; however, it is important to distinguish between Singleton as a design pattern, and a single instance of an object. In the latter case, the responsibility of creating and maintaining a single instance lies with the application itself. Typically, this is handed with a factory or Dependency Injection container, which creates a single instance somewhere near the “top” of the application (i.e., closer to an application entry point) and then passes it to every object that needs it. This approach is absolutely correct, from both testability and API quality perspectives. The new OperatorNewing up an instance of an object in order to get some job done introduces the same problem as the Singleton anti-pattern: unclear APIs with hidden dependencies, tight coupling, and poor testability. For example, in order to test whether the following loop stops when a 404 status code is returned, the developer should set up a test web server:
However, sometimes
It is also OK to create a small, temporary object that does not produce any side effects, except to modify their own state, and then return the result based on that state. In the following example, we don’t care whether
Static MethodsStatic methods are another potential source of non-deterministic or side-effecting behavior. They can easily introduce tight coupling and make our code untestable. For example, to verify the behavior of the following method, unit tests must manipulate environment variables and read the console output stream to ensure that the appropriate data was printed:
However, pure static functions are OK: any combination of them will still be a pure function. For example:
Benefits of Unit TestingObviously, writing testable code requires some discipline, concentration, and extra effort. But software development is a complex mental activity anyway, and we should always be careful, and avoid recklessly throwing together new code from the top of our heads. As a reward for this act of proper software quality assurance, we’ll end up with clean, easy-to-maintain, loosely coupled, and reusable APIs, that won’t damage developers’ brains when they try to understand it. After all, the ultimate advantage of testable code is not only the testability itself, but the ability to easily understand, maintain and extend that code as well. Further Reading on the Toptal Engineering Blog:
What form of testing involves viewing the software unit as a device that has expected input and output behaviors but whose internal workings are unknown quizlet?White-box testing involves viewing the software unit as a device that has expected input and output behaviors but whose internal workings are unknown.
What are the 3 types of testing?The different types of tests. Unit tests. Unit tests are very low level and close to the source of an application. ... . Integration tests. ... . Functional tests. ... . End-to-end tests. ... . Acceptance testing. ... . Performance testing. ... . Smoke testing.. Which testing type is for testing the system behavior?Black Box Testing is known as Behavioral Testing. The testing of external behavior of the program is known as Black Box Testing. It can be applied in software and any program. The main aim of black box testing is what is input and what gets output.
Which testing is used to test the code?1. White Box Testing. White box testing involves testing the product's underlying structure, architecture, and code to validate input-output flow and enhance design, usability, and security.
|