A comprehensive guide to Angular unit testing

Strategies and pitfalls using the default Jasmine and Karma frameworks.

Gergo Bence Szucs
Betsson Group
8 min readDec 18, 2019

--

In the past years, test-driven development (TDD) has become a standard in software engineering, as it inspires to achieve clean code by enforcing developers to think twice before typing 👌. In this article, we will take a closer look at Jasmine, a behavior-driven development (BDD) framework to create unit tests and Karma to run them in pre-configured environments while using Angular.

Image by Santiago GarcĂ­a da Rosa

The Angular CLI takes care of the configuration of these frameworks when you create a new project with the command ng new my-app. The karma.conf.js file in the main directory of your new project is used to fine-tune Karma’s settings, while the *.spec.ts files are the ones where you can define your Jasmine unit tests.

Running ng test commands Karma to search for the spec files in the specified folders (as defined in thekarma.conf.js file), evaluate every test and present the results in the console as well as in a new browser window (depending on the configuration).

Letting ng test continuously run in the background is a great practice, as it will recompile and update the test results anytime one of the source files is modified.

Style conventions

One of the trendy ways to write unit tests is the AAA (Arrange, Act, Assert) style. With this method, tests start with setting up initial values (arrange), then changing them in a reasonable way (act) and finally checking whether the modification achieves the desired outcome (assert).

In case of similar unit tests, it’s possible that the arrange sections are exactly the same. It may seem logical to move them to a common function that runs before each test (actually there are utility functions like this in most unit testing frameworks, we will cover those later). However, in unit testing, the DRY (Don’t repeat yourself) principle doesn’t have to be strictly obeyed, some DAMPness (Descriptive And Meaningful Phrases) is reasonable to make sure tests can be understood individually, hence it is absolutely fine to retain the redundant arrange segments. Generally, you can’t go wrong with either solution, in the end, it all comes down to personal preference or project conventions 😊.

There are two major types of unit tests in Angular. Both of them should be easy to write and test only one thing (multiple assertions for the same behavior are good). However, isolated tests are for components only, while integration tests are also testing the associated template file and external dependencies. So let’s start with the former, as it’s pretty much just a stripped-down version of the latter.

Testing in isolation

As we mentioned earlier, an isolated unit test only covers the component code. Let’s start by examining the automatically generated unit test in our new Angular project.

The describe() function is the test suite, it wraps the individual test specs (it() functions) and optional inner test suites (embedded describe() methods) together. Using the first string parameter for both utilities, you can define a human-readable description for the tests. The name of a test suite should be the name of the component, for inner suites the name of the component’s functionality (a good way to group tests together) and for the test cases, it shall start with ‘should’ and clearly describe the intention of the given test. The expect() function compares its parameter to the provided value using matchers. You can read more about the built-in matchers here.

As mentioned earlier, Angular also provides setup and teardown functions, where you can write code to be executed before or after the individual test cases. These are beforeAll(), beforeEach(), afterAll() and afterEach().

Enable/disable test cases

It’s also possible to control which tests should be executed. You can omit test suites or individual tests from the test run by prepending an ‘x’ to their name (e.g. xdescribe() or xit()) or you can focus on them to make sure only the marked ones will run by prepending an ‘f’ to them in a similar fashion.

Testing a simple service

Angular has a built-in dependency injection framework that most components use in a real application. However, a component level test suite shouldn’t focus on testing the injected dependencies (many times those are coming from external parties anyway). Consider this straightforward implementation for a service that handles user logins.

Testing this service (without any dependencies) is quite easy.

However, such mere code is unique, due to the nature of Angular, you will probably have dependencies in your components, let’s see what you can do in those cases.

Mocking with spies

Assume that you have a component that uses the previous login service.

When testing this, you don’t want to worry about the injected LoginService, its implementation should be tested in another test file. We could create fake objects, override functions, but these methods will be cumbersome for complex services. Thankfully, Jasmine provides a way to make the injected components behave like we want them to, using spies.

As you can see, using a spy on an existing service is really easy and handy, you can simply override the return value of a function which your component depends on. Imagine if you would have an HTTP call in the login function, you can simply override the function’s behavior to return a successful response code or an object with a dummy value.

Angular TestBed

There is one thing however in the previous example that we may want to change. Instead of initiating the dependencies manually, we could rely on a handy solution provided by Angular, the TestBed, which mimics the context of a real Angular app for our tests. Apart from handling dependency injection and change detection, it also allows the testing of templates and user interactions 😎, which we will need in our integrated unit tests.

Using the TestBed is rather straightforward as you can see. It doesn’t necessarily alter our test cases, we just get the dependencies and the components handled in a more convenient way.

Async testing

There are many ways to test async functions with Jasmine, the most important are async with whenStable() and fakeAsync in conjunction with tick().

The first solution keeps the async logic for the test as seen in the example below.

The detectChanges() method is responsible to trigger bindings manually as it isn’t done by Angular when running unit tests (you would also need to manually fire lifecycle events, e.g. ngOnInit(), ngOnChanges() etc).

The other solution is to use fakeAsync() which allows us to write our code without callbacks as it was synchronous, by stopping the execution using the tick() method.

The tick() function can take one parameter, which is the time in milliseconds to skip. However, this would be hard to keep track of, so you can call it without a parameter. In this case, it skips to the point when all pending async tasks are resolved 👍.

Non-inline template & CSS files

As a rule of thumb, fakeAsyncis preferred, unless your test involves XHR (XMLHttpRequest) requests. In case of Angular, this refers to the case, when you implement your template (HTML) and stylesheet (CSS) code in separate files instead of inlining it in the component and they need to be loaded. In this situation, you have to call the compileComponents() method, which results in the following changes in the TestBed configuration.

The beforeEach() must be asynchronous as the compileComponents() call resolves the template and style files in an async fashion. This also means, that our initialization statements must be moved to the then() callback method to make sure the TestBed is configured properly before executing them.

To emphasize the previous statement, when using this XHR based TestBed initialisation, the fakeAsync test method can’t be used ⚠️.

Using mock objects to remove dependencies

Sometimes you’ve got dependencies so complicated that it’s wiser to completely mock them instead of spying on their functions. By mocking an object, you can specify which methods will be accessible. These functions will lose their implementation, however, you can track whether they were invoked (the original implementation should be tested in the proper component though).

The example above shows how you can effortlessly construct a mock object with Jasmine, specify the available functions and substitute the original dependency. With this solution, your component’s code will execute without calling through the original function, but you can track whether that call was actually made.

Stubs and schemas

Mocking completely removes the component’s dependencies, however, in complex projects, you will probably have custom HTML directives, for example <app-banner></app-banner>. Your component should not be testing this, however, Angular would require you to declare it with its nested components and injected services. You probably don’t want to do that, so here are two solutions to assist you.

Firstly, you can create stubs for the custom directives, by overriding their selector with an empty (or a simpler) class definition. On the other hand, you can simply skip the stubs and assign a schema to the TestBed.

The NO_ERRORS_SCHEMA bypasses every non-Angular directive and property, while CUSTOM_ELEMENTS_SCHEMA bypasses every element with a ‘-‘ in their name.

The problem with schemas is that they can prevent the compiler from warning you about missing elements that you may have simply omitted or misspelled. For example, if you create an HTML input and assign a ngModel to it without importing the FormsModule, you will not get any errors during testing, while using the schema!

The following sample shows how you can adopt these two approaches in conjunction. It is a good practice to stub important elements, and use the schema to suppress warnings from the others. This is called shallow testing.

There is much more to unit testing in Angular, but these core concepts should cover most scenarios. Hopefully, you are ready to dive into testing your own project now! 💪

iGaming is a fast-paced industry where you need to be at the top of your game. Ready to take on the challenge? Apply for a position with us: betssongroup.com/career

--

--