Unit Tests –What? Why? How?

Gal Ilinetsky
CodeX
Published in
4 min readAug 24, 2022

--

As a computer science student, I didn’t really understand the core reasons for writing unit tests. When I wanted to push my code for the first time and was waiting for a code review, I got one comment — “where are your unit tests?”.

When you say unit tests, what do you mean?

As the name already implies, this is a way to test a unit — the smallest piece of code that is logically isolated. In a unit test, we must not try to involve integration between components, and we intend not to rely on outer resources (such as databases, web service, etc.). As unit tests are isolated from outer resources, we should be able to run them locally or on any other machine.

Unit tests are also part of TDD (Test Driven Development), a software development process relying on software requirements being converted to test cases before software is fully developed. During this process, test cases for each functionality are created before new code is written.

Today there are many tools to run, create, and write unit tests such as: VSTest, Karma, MSTest ,NUnit, XUnit, Junit.

What are the core reasons for unit testing?

The most commonly heard reasons are:

  • Covers cases we cannot test using manual or other software tests, such as component/system/automation tests, especially edge cases.
  • Early problems identification.
  • Quick execution runtime, which is great as a first layer testing before applying code changes to our source control.

But….

When I think about unit tests, the first phrase that comes to mind is Dependency Injection. Transferring the task of creating the object to someone else, and directly using the object, is called dependency injection. This helps us to follow SOLID’s dependency inversion and single responsibility principles.

There are several benefits from using dependency injection rather than having components satisfy their own dependencies. Some of these benefits are:

  • Readability: makes it easier to see what dependencies a component has
  • Extensibility: relying on abstractions instead of implementations, code can easily vary a given implementation
  • Maintainability: following SOLID principles
  • Testability

When dependencies can be injected into a component, so can a mock of those dependencies. Mock objects are used for testing as a replacement for real implementation. We can configure different behaviors to the mock objects, and that way we can test the component to handle all behaviors correctly. For instance, we can test if the component can handle when the mock returns a correct object, when null is returned, and when an exception is thrown. In addition, mock objects normally record what methods were invoked on them and how many times were they invoked, so the test can verify that the component using the mock, have used them as expected.

It is quite clear that if we will try to write tests to components that don’t follow the dependency injection pattern, we will soon find that many scenarios or even parts of the component behavior are not testable.

For example, if our component uses a network layer component (such as HttpClient) and we don’t have a dev url, or the machine our tests are running on is blocked from reaching the internet, we will not be able to test all behaviors that use the HttpClient functionality if we don’t use dependency injection.

When we write unit tests, it forces us to write more readable, extensible, and maintainable software that follows SOLID principles.

How do we write a unit test?

A unit test should be written similarly to how we write an article.
In an article, we have a title, introduction, the article substance, and a conclusion.
In a unit test we have a test name, preparations, execution of the component’s behavior, and a decision:

  1. Test Name: should include what the test does, and the expected result
  2. Preparations: creating mock objects with specific behaviors, and injecting them to the tested component
  3. Executing component’s behavior: run the method we want to test
  4. Decision: compare expected result to actual result, and decide if the test failed or passed

Here is an example:

We have a class CarProvider that has a dependency of IHttpClientWrapper with a method GetCarPrice(string model, int year) that when given a model and an year, the method will return the price.

The test name describes that we expect to get 0 as a returned value from the method GetCarPrice when an exception is thrown.

In the preparation section we are creating the mock object of IHttpClientWrapper that we inject into the CarProvider component, and setup it to to throw an exception. We then inject the CarProvider component with the mock object.

In the execution section we call the method we test.

In the decision section we compare the result we got to the expected result, which is 0, and decide if the test failed or passed.

So don’t wait for the code review, start writing unit tests 😉

--

--

Gal Ilinetsky
CodeX
Writer for

Software Engineer, .net development focus. Here to share my knowledge on points of view on software development fields I take interest in.