In my previous post, I talked about the Basics of General Testing. It was a short introduction into the different types of testing and how to approach testing in Jasmine.
In this article, I want to take a look at the auto-generated unit test files of an Angular Application and explain what's happening. I'll unit test services for my examples since it's a good place to start understanding the fundamentals. Also, Angular Components have a bit more going on under the hood and that requires an article of its own.
Many Angular developers get confused and even overwhelmed by what's happening in a .spec.ts
file. This article will change that.
Code for this article can be found here.
Angular Services
Services are by far the easiest to unit test. They're simply Injectable classes that may or may not have some state and contain a collection of functions. However, they shouldn't be taken lightly. Your services are where all your business logic should be. Therefore, testing them as much as you can will prove useful.
Let's take a look at an InventoryService and its auto-generated .spec.ts
file:
There's a few points to note here.
There's a describe
block that groups all our tests together for this service. A variable called service
is also initialized.
The beforeEach
block contains code that is run before every single unit test in this spec file. It helps us have a clean slate before running each test so that previous tests don't interfere. This helps with the essence of unit testing (testing one thing in isolation without any external factors).
There are other blocks of code like this to help us maintain clean tests, namely beforeAll
, afterEach
and afterAll
. You can read more about them and more pretty cool testing tools in the Jasmine Global API.
Looking inside the beforeEach
block, we see 2 unfamiliar pieces of code. TestBed.configureTestingModule
creates a dummy module for us to work with. TestBed.inject
initializes our service and injects it into that dummy module. This holds true for components, pipes, guards, etc. This is the unit testing philosophy combined with Angular's architecture.
An Angular application must have at least one module so a dummy module is created with only the piece of code being tested (in this case, the service) and nothing else. This way, nothing else from the outside can interfere with the tests. Pure Isolation.
Each it
block of code is a unit test. it
is a function that accepts 2 arguments: A string describing the test and a function that must contain an expect
assertion function. This expect
function is what Jasmine runs to assert expected values against actual results.
it('should be created'), () => {...}
is a unit test that is always created for any component, pipe, service, etc. It doesn't make sense looking at other unit tests if we can't initialize our code in the first place.
Building our Service
Let's say that I wanted to add some code to track the number of items in my inventory and a way to increment and decrement the amount.
Remember! We're approaching this from a TDD standpoint. We can write empty placeholders and then Tests first!
We write tests for what we want our code to do and then consider other cases. Tests for main functionality should look something like this:
You'll notice I used fdescribe
as opposed to describe
. This means "Focused Describe" and Jasmine will only run this suite of tests instead of all tests in the Application.
When we run our unit tests, we'll notice some failures (as expected).
It's telling us that the "should increment the count" and "should decrement the count" tests are failing. This is expected since we didn't write any code in there as yet. Let's change that.
And now our tests are passing:
The more astute among you might have realized that our decrement function isn't finished. We shouldn't be able to decrement if the inventoryCount
is already 0.
A reasonable test for this can be:
it('should not decrement when count is 0', () => {
expect(service.inventoryCount).toBe(0);
service.decrementCount();
expect(service.inventoryCount).toBe(0);
});
Re-running the tests gives us:
Our function currently decrements whatever value is stored in the service. We want it to decrement only when the value isn't 0. This is also a simple way to make sure that inventoryCount
never falls below 0 (assuming you want your system like that).
We can modify the function to be:
decrementCount() {
if (this.inventoryCount === 0) return;
this.inventoryCount--;
}
Now our tests are passing again.
Conclusion
In this article, we covered the following:
- The basic structure of an Angular Unit Test
- What happens in the
beforeEach
block - Why a dummy module is necessary for testing
- Building an Angular Service using TDD
There are a lot more of these articles to come where we'll dive deeper into services, mocking, component life cycles, etc. Stay tuned for more and thanks a bunch for reading! ๐
Top comments (1)
On decrementCount() if the inventoryCount is 0 you return nothing. For which cases to throwing an error could be more convenient?