Table of Contents
What is Unit Testing?
Explaining Unit in Unit Testing
This is a confusing question for most people when they start thinking about writing tests for their software. I know that I didn’t fully understand what “a unit” was in the context of testing until I wrote a lot of tests.
There are differing definitions of what a unit actually is, but it’s often defined as “the smallest component that it makes sense to test”.
For object-oriented programming, this could be a whole class or an interface. If your code has a functional programming approach, it could be a single class method or just a function. It’s often up to the development team to decide what their own definition of a unit is and how they want to test their units. For instance, our team has currently deployed some high-traffic Node.js applications. We write more functional JavaScript so our “units” tend to be exposed functions from modules. These may either be simple functions, for example code which validates user inputs. Alternatively, we have other “units” that are more complex, for example asynchronous code which calls 3 different APIs and combines the results and stores user cookies. You can see that although they have differing complexity, they’re both classed as “units” for testing purposes.
To test a unit you simply have to define your expectations of what you think it does, given any of the specified inputs. That’s it! If you’re the developer in charge of writing the code then the test defines what you know will happen. If you didn’t write the code under test, for example when testing against 3rd party or legacy code, you can write your tests based upon your expectations of what you think should happen. If either of these things change then you want your tests to fail. Your unit tests then define a solid contract with your code.
Explaining Unit Testing through Analogy
I think a good way to look at this is by analogy. Instead of software, imagine yourself building a car. A car has an engine, a transmission, a steering system, brakes, a cooling system, seat belts, headlights, and so on. A car is the sum of these parts/subsystems. Each of these parts has specifications and requirements that define expected functionality, tolerances, and interfaces to the rest of the system.
Finding the First Place to Test
Imagine the point in time when all of these systems are brought to the assembly line, and bolted together, without any kind of testing. The test driver gets in the car, and it doesn’t start. Or it starts, but does not move forward. What subsystem is to blame? If no unit testing was done before integration, trying to hunt down the problem is immensely more complicated.
Assuming it does start and move forward, will the testing done by the test driver ensure that the headlamps will last the required MTBF, that the cooling system will function at extreme temperatures at high RPM, or that the engine will function properly if the oil level is at it’s lowest allowable and highest allowable levels in sub-zero temperatures, as required by the design? These sorts of things are arguably best tested by the specialized groups that develop them, since they are best equipped to put the individual components through the sorts of tests that stress components in ways that final, integration, or end-user testing is not capable of doing.
And so it is with software. Like cars, software is a collection of independent components. String libraries, math libraries, UI toolkits, and so on, analogous to transmissions, brakes, and headlamps. The quality of a system built from these components is no better than the quality of the least tested component. When you write a component, unit testing helps ensure that no matter what might use the component, or how it might be used, it will be able to withstand whatever inputs and contexts it is subjected to, and work as advertised when it can, and fail gracefully when it can’t.
Unit testing should ideally be a tier-1 part of your workflow. Test-driven development helps to ensure this. By test driven, I mean classes, or related sets of classes that you design and implement should have accompanying test code (a test function or main application, for example) that exercises the code and its interfaces with an eye towards providing coverage for all expected inputs. Often, writing the test cases helps to flesh out an API before anyone attempts to use it. Detecting the failure of a unit test to compile or run should be a part of the lifetime of the component, something you do before checking it in, regularly as changes are made (to the component itself, or components that it depends on); if your component stops working, it often will be much easier to identify and fix a unit test failure than to root cause a bug to a component in a large system. Not to mention, it is much more efficient for everyone if you deal with bugs before they hit the mainline product and bring everyone else in development to a screeching halt.
Anyway, think about this the next time you get behind the wheel of a car — would you rather drive a car that had its brakes tested heavily (and regularly) by the brake engineers before it was delivered to the assembly line to be bolted onto the car, or would you trust that testing to what the integration test driver subjects the car to? The same sort of thought process should be used when designing software — testing is not something only done after all the parts are combined, test-driven development helps ensure that components are in good shape before they are assembled into a larger software system. Building unit tests from the start, and running them often during development whenever changes to the component, dependent code, or toolchain changes occur can go a long way to helping ensure the quality of the software that uses them, and help to identify problems before they reach QA or end users.
What is the Purpose of Unit Testing?
The most important thing that Unit Testing allows for, is the confidence that allows you to make further changes.
Programmers get piece of mind and more confidence in their work.
The programmers has many benefits from writing and having unit tests. The company that owns code also has many benefits too. Less time wasted debugging. Defects found earlier or prevented. Less side effect defects. Safer code changes. Extended code life. Fewer defects in the field. Happier customers.
So in Unit Testing, all non-trivial software will evolve as new requirements or features are identified and all non-trivial software can probably benefit from refactoring. When adding new functionality or tidying up existing code it can be very easy to accidentally break something that previously worked. This can often be something entirely unrelated to the change being made or caused by the developer making the change not fully understanding all the existing features. Fixing bugs caused like this can feel like playing whack-a-mole – the fix itself can cause another bug, fixing that causes another and so on. Developers can fear making changes because they don’t want to miss something and introduce a bug.
With (good) unit tests developers can be more confident in the changes they make because mistakes that break unrelated things will be caught.
Unit tests tell a programmer that their code is doing what they think the code should do. BTW: this is a big deal. Many software systems work by accident due to offsetting defects.
With automated unit tests, the cost of retest is zero. That means the unit test can be re-run with every change. Consequently, unit tests can notify the programmer immediately when a code change modifies some other tested behavior. Unit tests are a safety-net.
Comprehensive unit tests allow the programmer to improve the design of their code. Mistakes are caught in the safety-net.
As the complexity of the code under test increases, this becomes evident also in the test cases; the tests get complex to write and maintain. The astute programmer can use this as a indication that the code should be refactored to improve and simplify the design.
Manual Vs Automated Unit Testing Methods
Unit tests are also defined to be methods that test a small chunk of code grouped together to form one logical function.
Manual Unit Testing
These methods can be manual or automatic. When unit testing a chunk of code manually, which is often done by the developer himself, he/she would try to test out that the expectation from the chunk of code added by him/her works as expected. Often it also involves on a high level checking if the overall product is stable after the addition of the code.
Automated Unit Testing
Automated unit tests are chunk of code written along with the code modified/added so that the logic of the new code can be verified without manual intervention. This tests are like safety net that are added one after other so that your new code is also compatible with the previous expected functionality. Few important characteristics of the automated unit tests are that it should run in memory, should not rely on network and should test only smallest possible chunk of logic expecting everything else to work as expected.
This is often achieved via mocking. In this process, you are expected to mock every other code that interacts with the code in test to behave as expected and then expect the code in test to behave as expected too. This only works if the interacting code has similar tests.
Unit Testing in Java
They look like this in Java:
@Test public void returnsSumOfTwoNumbers(){ assertThat( addTwoNumbers( 3, 5 ), is( 8 )); }
This tests a function called addTwoNumbers that takes two numbers. It supplies 3 and 5 as example numbers, then checks that the returned value is 8.
If it isn’t 8, the test runner would give you a red bar, and report all failed tests.
They are very useful to ensure your code does what you expect it to do, to ensure that future changes don’t accidentally break things, and to help you think about the design of public interface of your code.
The last point is critical to TDD. You’ll probably assume that addTwoNumbers() is implemented using the addition operator. But it could just as well look up the answer in a database, or a lookup table, or call special hardware. The test would still pass, and be unchanged, as it only tests the public interface of our function.
Unit tests also are coded to avoid relying on environmental factors. So they do not rely on a database being in a particular state, nor do they scrape web output. There are tests that do this, but generally get referred to as integration tests or system tests.