IT Tips & Insights: Learn why unit tests are so important, and how to build them in this step–by-step guide from a Softensity Software Engineer.
By Bruno Belo, Software Engineer
Who needs unit tests?
“Oh, this is easy”, “It’s a tiny little change, it will be fast”, “Good, I already know what to do” … these are common thoughts that precede a new bug after a software change. Very often, developers tend to underestimate their activities, the complexity of their legacy/new software and how easy it is to break an existing feature without even noticing, until someone else complains about it. And that’s why: Everyone needs unit tests.
Any software that you want to be reliable, easy to maintain, and easy to evolve needs to be tested in many different ways, very often, and unit tests are the first line of defense against unpredictable/unwanted outcomes
Test Pyramid
The test pyramid shows the difference between test software types. The closer the test is to the final user, the more time it will take to build, to maintain, to run and to fix, if needed. In other words, the more it costs.
As the image shows, the unit tests are the ones we want to have most frequently in our applications as they are the base to any other tests. They are easy to build, easy to maintain, and fast to run when compared to other tests. They should be the first tests to cover core functionalities of any piece of software as they alert us when an unexpected behavior is introduced in our applications.
But what hack is a unit test?
In simple terms a test is an assertion that something is working (or failing) as expected. Therefore, a unit test confirms that a small piece of work/functionality is producing the expected behavior and expected behavior only (angular wise: single component, single class, single directive, all sorts of pieces of code that should have clear defined behaviors). In terms of software, we can think about unit tests like small contracts, the commitment of a given code to a given behavior and result.
Building Angular Unit Tests
Front-end applications have gained a lot of responsibility over the past years. They’re far from being the simple HTML and small Javascript functions that they once were. Let’s dive into ‘The Most Trusted Angular project’ and see how to build good and meaningful unit tests for this modern web applications written in Angular.
Let’s create a new Angular Project by running the following command line:
By default, our new project is created and configured to use Jasmine testing framework, which provides a large set of features to build tests, and also Karma, a tool that’s capable of executing javascript code in real browsers and facilitates the test’s debugging.
Every new component generated by angular CLI, besides the component-name.ts and style files, also has its own test named as component-name.spec.ts. In our new app we can see the app.component.spec.ts file which looks like this:
Now let’s understand the core components of a test written in Jasmine and present in our app.component.spec.ts file.
- ‘describe’ method: It is the entry point of tests, and groups a set of unit tests. Usually each .spec.ts file should have only one ‘describe’ method related to its component/service/class. The describe method has two parameters:
- description: name of the group of unit tests
- specDefinitions: object composed by Jasmine function specifications
- ‘it’ specification: It is an actual unit test that must have one or more assertions using the “expect” chained with one function Matchers such as toBeTruthy(), toBeTruthy(), toContain(). Each it specification has two parameters:
- expectation: string with description of what behavior is being tested that usually starts with “should” or “should not” do something and can be concatenated with “When” for more elaborated conditions
- assertion: a callback function that must have one or more assertion, usually following the Tripe A test pattern (Arrange, Act, Assert)
- ‘beforeEach’ specification: is the place where we configure our test suit and, as the name suggests, is executed before each it specification
Whenever testing an angular component, we also need to initialize the TestBed which will be used to configure the component dependencies such as modules and services.
So now that we covered the basics of our app.component.spect.test lets run all our tests using the command:
As output of this command Karma handles the browser run and tests execution showing us the following output:
Yay! All tests are good, 3 specs succeeded and 0 failed. That’s good and also expected. Now lets create a new component with some basic logic and dive in a bit more to identify a good unit test approach.
You can get all application changes here: TheMostTrustedAngularProject.
Basically we have a new ‘Guess the Age’ component that will be responsible for receiving a person’s name as input from the user, and communicating with the guess-the-age service which will in the end call the free API https://api.agify.io that has the ability to predict someone’s age by its name.
Our guess-the-age component looks like this:
Looking at this small component, there are few behaviors we want to protect and keep the same as the application evolves. Usually these behaviors are founded as being the functional requirements or acceptance criteria of any given work.
In “GuessTheAgeComponent” the most important one would be:
- age should be displayed after a name input changed
Since we want our application to grow healthy and sustainable, we should use unit tests to validate the requirement and future-proof it.
First we configure our test unit using the beforeEach specification. It will look like this:
We want to test our component only, and to do so, we need to provide a mock for all its dependencies so we can easily manipulate the expected results and make sure that we are testing nothing more than what we really want to test. In the case of GuessTheAgeService, we are injecting it using a special object created by Jasmine createSpyObj method.
Whenever performing unit tests, it’s common to have both wanted and unwanted aspects of the same behaviour to avoid falsepositive, false-negative cases so we will have two ‘it’ statements to validate our first case:
We have a few new things going on here so let’s check them out, one by one:
- Fixture: The jasmine Fixture encapsulates the DOM and provides us useful manipulations such as detectChanges, component instances and etc.
- DebugElement: The debug element is a very handy fixture property and gives us access to the actual rendered DOM. In order to have a good unit test component we should validate the html elements generated in the UI because they are in the end what is seen by the user and used for interactions.
- fakeAsync/whenStable: In our component the user interaction has a type of input timeout, to avoid unnecessary processing. Therefore, in our unit tests we need to make sure that we reproduce this waiting time before checking any expected results.
Our tests are completed, our component is secure and the most important aspects are:
Both tests we are validating the same code from different inputs to avoid false-positive and false-negative.
We are not testing anything besides our component, the only dependency is being mocked with our jasmine spy object.
The tests are the closest as they can be to a real user interaction. We are interacting and validating the DOM elements, not just checking internal component behaviour such as private methods (internal aspects can change without any test failure, the contracts and expected results cannot).
Angular Unit Testing Tips
Run a Single Test or Single Test Suit
Often when debugging a large system we have many tests which are already working. To save time we want to run new tests only during debugging. To do so we just need to add the f letter before the describe and it specification methods turning then to be fdescribe and fit respectively.
Skip Single Test or Single Test Suit
In the same way sometimes we want to skip a particular test or tests in a large system. To to do so we just need to add the x letter before the describe and it specification methods turning then to be xdescribe and xit respectively.
Common Angular Module
A common angular module can and should be created to avoid code duplication when configuring the component’s unit tests.
Learn More
Often we doubt how small a unit test should be, if it is starting to be an integration test or not. A new terminology is being used called the Swiss Cheese Test Model. You can find more information in the awesome post.
About
Bruno Belo is an experienced Full Stack Developer with an MBA in Computer Software Engineering and certifications in C# and Azure Foundations. He has more than 8 years of expertise in crafting high-quality code and solving complex challenges. Bruno is skilled in .NET Core, Azure, and Angular, adhering to SOLID principles and agile methodologies. He is committed to creating impactful software solutions that drive positive change.