Deep dive into Angular component DOM testing

Angular Dec 27, 2022

Unit Testing is an important aspect of every Web Development. Having Advanced Unit Test will cover more component logic and increase the quality of product development.

In the previous blog on AngularUnit Testing,  we talked about pure component class testing. Though, component class testing is more than sufficient to validate most of the component logic; it falls short in validating component class and template interaction, which is indeed an important aspect of web development.

In this blog, we will dive deep into component testing and talk about DOM testing and we will also go through some of the important aspects of unit tests that we should be knowing for creating TestBed. We will also cover writing some performant UTs.

What is Component DOM testing?

As Angular rightly defines, the component is more than just class itself. it's a combination of template and class. DOM testing can be defined as a way of Unit testing a component class from a template perspective or user perspective. It involves testing the component in its entirety. It covers all aspects of user interaction from a click of a button to the colour of a button that the user sees in the application.

Angular Component Structure

DOM Testing over Automation Scripts

Though we have automated integration tests in-place to check  and validate the UI behaviour, a developer would not know the issue  before hand especially   when there is a time gap between development and running automation  scripts.

Dev errors are inevitable and can occur both in creating a new component and upgrading the existing component. Pure component class testing fails to identify errors in the following scenarios

  • When logics used in directives like ngIf and ngClass are left unattended after a component upgrade
  • if any child components are used within the template and its attribute is not defined correctly.
  • Any random texts introduced in a template that are unintended to be there.

we might ask if these error wouldn't be catched while building the TestBed, the answer is when writing unit test focused on component class, we add schemas to bypass the errors, which will be discussed later in this article.

We cannot substitute Automation scripts  with  component DOM  testing, However it helps in early catching of  error  and avoid QA dev cross iterations in case the above mentioned errors were introduced.

Getting started with DOM testing

We will take a pretty common UI example mostly used in e-commerce applications; let's define a child Dumb UI component called AddressOptionsComponent where the user will be prompted to select addresses from the savedAddress and proceed, template of the component is shown below

Address Option component template 

the component  class is as below.

Address Option component class

this component is used inside a smart parent component which binds the savedAddresses to the @Input and listens to the event raised by the `@Output` property, This is the data source for addresses and events triggered after address selection to process further in the parent component respectively.

We have defined three simple methods inside the component viz, disableSubmit(), addressSelection(), submitAddress().

Advantages of DOM testing over class testing

Usually, when we write Unit testing for component classes we would write the test as follows, We will skip the Test bed setup  here since we have already talked about it in the previous blog, the tests will be like

address option component class test

Here, in the test file, we can see, we create component Instances and logically verify that each method works as it intends to be. But we do not validate core workflows and user interaction like mentioned below

  • Interaction between Parent and child component
  • What users see on UI.
  • User interactions like selecting an address/clicking on submit
  • If predefined ng directives like ngIf and ng-template are used in the template itself.

We write Component class tests on the assumption that the template works well if the class is error-proof, but it might not be the case always. hence DOM level testing comes to rescue.

Writing your First Component DOM test.

When we create a component Instance with TestBed like TestBed.createComponent(), along with creating a component instance, it adds a related template to the test runner and returns a Component Fixture.

We can access the template of the Component fixture by fixture.nativeElement however this is risky as it needs us to be running our unit tests in a browser like ChromeHeadless. This is a problem when we want to run unit tests in a non-browser platform that doesn't support DOM like CI/CD pipeline.

So we will employ a much safer alternative fixture.debugElement.nativeElement This returns the platform-specific element object instead of creating an HTML element tree.

Now, let's add a few more tests which mimic user interaction or you can say it does component DOM testing.

address Option Component DOM Test

This test validates the header shown for selecting the address, which wasn't tested in class testing. We do that by querying the debug element with id #address-header using By from platform-browser package like By.css('#someId') and then we get innerText of the element with its nativeElement.innerText. later we expect the header element to be what we intended it to be. There are few other ways to query the debug elements other than id, some are element, .class,` #id, and  [attribute]

Testing User Interaction via Events

Lets now see how do we test interaction , the test goes like below

component DOM-Claass interaction

similar to the header text we saw earlier, we query the debugElement, but now we have used the input element to get all the elements within the input field, we could also specify id in case of a complex project with multiple input element. one of the other ways of getting hold of the input like radios is to use ng-reflect-value.

Now we try to access the first option element and try to trigger an event for ngModelChange mimicking the user selecting one of the address options. later we expect the selectedAdddress is the same as what the user clicked on.

you might see `fixture.DetectChanges() `is being called in multiple places, Here is the reason why: If you see the HTML template, it renders or builds DOM layer based on the addresses that's been input to the component. In the first statement of the test when we assign the mock address, the DOM layer is already rendered and it has not built radio button DOM yet. So when we do fixtureDetectChanges, it triggers a change detection cycle for the component and builds up the DOM layer for the radio buttons, In case the change detection cycle is not done after Data is fed into the component, our radio element will still be Null. We once again run the change detection cycle after we trigger ngModelChange to mimic user interaction.

We have used fixture.WhenStable , This is used to resume testing after events have triggered asynchronous activity or asynchronous change detection. We should note that when we are using await , The test should always be run in async.

Component DOM testing  ng Directive

Let us now see how can we test directives like *ngIf  For this We can take  simple example of displaying status, the template code for it will be like

ngIf directive example

in this component we check for the variable status and display Active/Inactive based on boolean value of  component  instance variable status.

Component Dom testing for ngIf 

For this in the unit test; First, we assign the status a boolean value true, and then trigger DOM rerender with fixture.DetectChanges and expect the text content inside the queried element to be Active. similarly, we set the status as false, and expect the textContent to be shown as Inactive

Few points to keep in mind when you write DOM testing

  • Add fixture.Destroy() in afterEach()=>{} block , it destroys the component after each run, and so you will not have component residual bugging you in your next it() block
  • This can also resolve hanging DOM  tree if there is any. which might result in conflicting results next time you query the template
  • Try to Avoid click()  on the nativeElements and end up in  error loops ; where as triggerEventHandler  can directly trigger the click event and is the right way to mimic user interactions

Disadvantage of writing component DOM test

Though DOM testing helps in error-proof UT, it has some negatives, they are

  • Since DOM interaction is involved and we callfixture.DetectChanges regularly; it rerenders the whole component which  can slow down Unit tests execution time.
  • tests become bulky even for simple component

BeforeEach vs BeforeAll ,which one is compilation heavy?.

We already have talked about the implementation of beforeEach() in my previous blog,  This could be compilation Heavy  as discussed below

  • If you have written  Component class unit testing  for some complex component, you might already be aware of the amount of Spy for services we create in complex component. Most of the cases, these spy needs to created or injected only once,  and they are never reassigned.
  • For them we can use beforeAll() instead of beforeEach, so the spies for services are crated only once.
  • TestBed.configureTestingModule(...).compileComponents() in beforeEach() unnecessarily compiles the component before each test, But for the most part, the testing module is going to be static for a particular .spec file. Hence configuring the module at each iteration of the test is adding time to the total time execution
  • Then we might ask, why cant we configure Testing Module in beforeAll()?, the answer is NO,  Angular has defined ResetTestingModule(), after each tests that resest the the testing module and it needs to be configured  before each test.
  • This condition can be fixed. by mocking the resetTestingModule itself, This can reduce UT performance by heaps, but can result in unstable UT setup.

To address this, recent updates of Angular introduced Ivy compilation, We need to enable it by setting enabledIvy: true in tsconfig.json. By this TestBed.configureTestingModule(...).compileComponents() gets faster by skipping later compilation of components if it already exists.

Schemas

In your testbed, along with providers, and imports, you can add one more attribute called schemas, which typically have the value NO_ERROR_SCHEMA with this we are saying our testbed ignores any template errors. This means that the compiler just ignores any elements it does not recognize, meaning you do not need to declare all the components used in the template of the component under test.

Working on DOM testing, and ignoring the template errors is the last thing you want to do, let us take an example of a parent component using AddressOptionComponent as a child, since this component has input addresses, and you wrongly miss spelt it while using the input, this error could be bypassed with NO_ERROR_SCHEMAS. which defies the purpose on unit testing.

In case if we are using  third party components like material components, we can use ng-mocks to mock the child component.

Nested Describe block Do's and Dont's,

Nested Describe block are indeed a better way to  write logically separate test for the  method of interest with different possible inputs, . but what we often miss is the common inputs or common  Function we tend to define in each it() block. for this beforeAll() come handy .we could also use beforeEach() if some data has to instantiated for each tests, we can take sample test for this

Here we have defined the mock variable in describe and we have deep copied the mock value in beforeEach and used  that in it() block.

Summary

In this blog we talked about benefits of Component Testing in detail and How DOM testing can avoid dev errors, later we went through ways of writing unit tests with reduced execution time. Hope this blogs helps you well in writing complex and performant unit test in angular.

careers.india@halodoc.com

About Halodoc

Halodoc is the number 1 all around Healthcare application in Indonesia. Our mission is to simplify and bring quality healthcare across Indonesia, from Sabang to Merauke.
We connect 20,000+ doctors with patients in need through our teleconsultation service, we partner with 1500+ pharmacies in 50 cities to bring medicine to your doorstep, we partner with Indonesia's largest lab provider to provide lab home services, and to top it off we have recently launched a premium appointment service that partners with 500+ hospitals that allows patients to book a doctor appointment inside our application.
We are extremely fortunate to be trusted by our investors, such as the Bill & Melinda Gates foundation, Singtel, UOB Ventures, Allianz, Gojek and many more. We recently closed our Series B round and In total have raised USD$100million for our mission.
Our team work tirelessly to make sure that we create the best healthcare solution personalized for all of our patient's needs, and are continuously on a path to simplify healthcare for Indonesia.

Tharun S B

Angular Developer