loading...
Cover image for A Guide for every UI developer with an aversion to Unit Tests.

A Guide for every UI developer with an aversion to Unit Tests.

divye1995 profile image Divye Marwah Updated on ・10 min read

Writing tests are analogous to tasting your meal before you serve it. The importance of unit testing is known at all levels in programming but more often is ignored when it comes to UI developers. This post is a brief about how you can start your journey in being a better Frontend Engineer by incorporating these key concepts about unit testing in your code.

Overview

  1. Importance of Unit testing

  2. Sample App

  3. Conclusion

Importance of Unit testing

Writing Unit Tests does seem like an overhead when a you can just test the functionality by using it. For the times you are in such a dilemma you can keep these few points in mind:

  1. Unit tests not only improve quality but decrease time to debug : Unit tests help you understand what parts of the app are working as intended and what parts aren’t and hence allow you to narrow down on the cause of bugs much quicker than using console.logs or debuggers.

  2. We are JS developers!! : We all as developers have either built test UI components and rough Html to test an underlying logic/service or delayed testing till our Presentational components are done. Writing a unit test allows you to iteratively build a functional component without unnecessary test UI elements.

  3. Freedom to collaborate : Working in a team I have often noticed members working on isolated silos of functionality and with a large code base there is a never ending fear of breaking some working code during refactoring and bug fixing. This should and can be avoided if you write proper unit tests along with the code which detects any breakage in case of changes for developers who may work on the code later.

  4. No Low level Documentation a unit test declares the purpose of a given unit of code. This reduces the requirement for a developer to explicitly document code ( would also recommend declarative style of programming for all JS developers) and product teams can focus more on the look and feel of the application than on the functionality.
    Using Test frameworks like Jest also allows you to test Frontend code on your CI/CD environments which is a plus to the point no. 3 as it helps generate regular reports on your code health and test coverage.

Here are some key guidelines that you should keep in mind while writing unit tests :
  1. Understanding the type of unit tests that should be written depends on the type of app component( Presentational, Logic Containers,Services,etc). Understanding what should be tested really helps in reasoning the extra effort you are taking in writing unit tests at each level.

  2. Write Functional JS and try to break down your app into Presentational and Logic components as much as possible. This really helps in improving the focus of your unit tests and also decreases the time taken to write them.

  3. Write Tests along with the code. This is By far the most Important one !! I can’t stress enough on how painful it has been for me to revisit old code and add unit tests for already developed components. It requires both time and effort to figure out what you have written and what to test. When Tests are written our aim should be to write code that pass tests rather than the other way around.

  4. Practice writing tests before you dive into writing your app. Most developers avoid writing tests because they either don’t know or are not completely sure about some basics like Mocking a Class , testing an async call, mocking http calls etc. Get rid of these confusions and myths with practice. So practice unit testing as much as you practice writing application code.

Having Understood the importance of writing tests we are going to go through an example Angular App and write some unit tests around it with Jest.

Why Jest ?

Jest is beautiful testing framework that provides a uniform and non-browser based unit testing options for multiple javascript frameworks.

Find more about them here.

Also a shoutout to jest-angular-preset library that makes it easy to use jest with angular. With jest I get three great features which aren't present with default angular testing setup : Snapshot testing, Unit tests that can run without browser and AutoMocking. I suggest everyone to understand these to use this wonderful framework to its fullest.

Setup :

If you have never used angular before please follow the official angular setup guide here

Our App will have three major Component : AppComponent, ListingService, ListRowComponent. But before we start writing our components and test cases, We have to setup jest.

Steps to setup jest :

Use this quick guide to do the initial setup, remove karma based code, and run jest.

Jest allows you to store your config in either a jest field in your package.json or in a separate file jest.config.js

I would suggest everyone should go through the official config guide once to know what kind of configurations your project can have and might need. To help you guys I would recommend at-least focusing on the following fields: setupFilesAfterEnv, coverageDirectory, coverageReporters, transformIgnorePatterns, modulePathIgnorePatterns, moduleNameMapper, testPathIgnorePatterns

Here is jest.config.js from our sample app


module.exports = {
    "preset": "jest-preset-angular",
    "setupFilesAfterEnv": ["<rootDir>/setupJest.ts"],
    globals: {
      "ts-jest": {
        tsConfig: '<rootDir>/tsconfig.spec.json',
        "diagnostics":false,
        "allowSyntheticDefaultImports": true,
        "stringifyContentPathRegex": "\\.html$",
        astTransformers: [require.resolve('jest-preset-angular/InlineHtmlStripStylesTransformer')],
      }
    },
    coverageDirectory:'<rootDir>/output/coverage/jest',
    transformIgnorePatterns: ["node_modules/"],
    "coverageReporters": [
      "text",
      "json",
    ],
    "reporters": [
      "default",
    ],
    snapshotSerializers: [
      'jest-preset-angular/AngularSnapshotSerializer.js',
      "jest-preset-angular/AngularSnapshotSerializer.js",
      "jest-preset-angular/HTMLCommentSerializer.js"
    ],
    "transform": {
      '^.+\\.(ts|html)$': 'ts-jest',
      "^.+\\.js$": "babel-jest",
    },
    modulePathIgnorePatterns: [],
    moduleNameMapper: {},
    testPathIgnorePatterns:['sampleCodes/'],
  };


Here is my tsconfig.spec.ts


{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "outDir": "./out-tsc/spec",
    "types": ["jest", "node"],
    "emitDecoratorMetadata": true,
    "allowJs": true
  },
  "files": [
    "src/polyfills.ts"
  ],
  "include": [
    "src/**/*.spec.ts",
    "src/**/*.d.ts"
  ]
}

Note: Don't simply copy and paste the code but understanding the config really helps you in setting up your entire config for your project on your own.

I would also suggest install jest globally

npm install -g jest

This really helps when running jest cli commands required for snapshot testing ( like updating snapshots using jest -u)

Finally run jest and check if the basic tests that are auto created with ng generate are running using

jest --coverage

Here is a great guide on how to test components and improve our test cases and how the DOM Testing library helps in this

Writing Unit Tests for Presentational components

If you are in practice of writing Pure Presentational components then you are awesome!!. If you aren't I suggest you start practicing on how to divide your app code into Logic Containers and Presentational Components.

Jest has the ability to use Snapshot testing for testing the UI components. Read more about Snapshot testing here

This saves time spent writing DOM queries. As per the documentation one should commit these snapshots with your code so you can verify how your UI components should be rendered in DOM.

When Not to use snapshots ?

If the component is basic and simple enough ,snapshot testing should cover most of your UI tests, though avoid using it with Presentational Components like Lists where you would want to check the total number of rendered rows or in components where verification of business logic representation is required.

Below Find Sample ListRowComponent


@Component({
  selector: 'app-list-row-component',
  templateUrl: './list-row-component.component.html',
  styleUrls: ['./list-row-component.component.scss'],

})
export class ListRowComponentComponent implements OnInit {

  @Input() firstName:string;
  @Input() lastName:string;
  @Input() gender:string;
  @Output() rowClick = new EventEmitter();

  getClass(){
    return {
      'blue':this.gender==='male',
      'green':this.gender==='female'
    }
  }
  constructor() { 
  }
  ngOnInit() {
  }
}

Below Find Sample ListRowComponent.spec file



describe('ListRowComponentComponent', () => {
  let component: ListRowComponentComponent;
  let fixture: ComponentFixture<ListRowComponentComponent>;


  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [ ListRowComponentComponent ]
    })
    .compileComponents();
  }));

  beforeEach(() => {
    fixture = TestBed.createComponent(ListRowComponentComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

  it('should create', () => {
    expect(component).toBeTruthy();
  });

  it('should render the component with blue color class',()=>{
    component.firstName = 'James'
    component.lastName = 'Bond'
    component.gender = 'male'
    fixture.detectChanges()

    expect(fixture).toMatchSnapshot();
  })
  it('should render the component with green color class',()=>{
    component.firstName = 'James'
    component.lastName = 'Bond'
    component.gender = 'female'
    fixture.detectChanges()

    expect(fixture).toMatchSnapshot();
  })

  it('should emit events onClick',done=>{
    let buttonClicked = false
    component.rowClick.subscribe(()=>{
      buttonClicked =true;
      expect(buttonClicked).toBeTruthy()
      done();
    })
    var btn = getByTestId(fixture.nativeElement,'row-click');
    simulateClick(btn);
  })
});


Note: If you notice I’m using data-testid to query the button in the unit test above. I would suggest all developers to bring this into practice , It makes our tests very resilient to change and robust in nature.

Writing Unit Tests for Services

First here are some concepts that were confusing to me before I stared writing unit tests for services or containers :

Mocking Dependencies. There are a lot of great tutorials available with a simple Google search on this but most use component constructors or promote using auto-mocking features of Jest for Mocking dependencies. It depends on your preference which method you use ,For me mocking dependencies while using Angular’s Dependency Injection to instantiate a component were key and I found a really good way to do that .

You can go through this great article about the same

Mocking Store : It is suggested that we should write getters and selectors for ngrx store ( https://ngrx.io/ ) in services , so that your components are reusable along with the store. This means mocking a Store in service becomes very important.

describe('Service:StoreService', () => {
  let backend: HttpTestingController;

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [HttpClientModule, HttpClientTestingModule, RouterTestingModule],
      providers: [
        provideMockStore({ initialState }),
      ],
      schemas:[NO_ERRORS_SCHEMA]
    });
    backend = TestBed.get(HttpTestingController);
  });

  afterEach(inject(
    [HttpTestingController],
    (_backend: HttpTestingController) => {
      _backend.verify();
    }
  ));

know more

Using Marble testing : Lastly Most Services you will create in your angular projects will use RxJs. To test your services and logic container components properly, understanding how to test these Observables ( best done using jasmine-marbles ) is essential.

Here is a great article by Micheal Hoffman that will help you get a good understanding about the same

Sample Service


@Injectable({
  providedIn: 'root'
})
export class ListingService {

  constructor(
    public http: HttpClient
  ) { }

  public getHeaderWithoutToken() {
    return new HttpHeaders()
      .append('Content-Type', 'application/json')
      .append('Accept', 'application/json');
  }

  public getHeader(tokenPrefix = '') {
    let headers = this.getHeaderWithoutToken();
    return { headers };
  }

  public doGet(url,header=this.getHeader()){
    return this.http.get(url,header);
  }
  public getList() : Observable<UserModel[]>{
    return this.doGet('http://example.com/users')
    .pipe(
      map((res:any[])=>{
        return res.map(toUserModel)
    }))
  }
}

Testing a service using jest


describe('ListingServiceService', () => {
  let service: ListingService;
  let backend: HttpTestingController;

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [HttpClientModule, HttpClientTestingModule],
      providers: [
        ListingService
      ],
      schemas:[NO_ERRORS_SCHEMA,CUSTOM_ELEMENTS_SCHEMA]
    });
    backend = TestBed.get(HttpTestingController);
    service = TestBed.get(ListingService);
  });

  afterEach(inject(
    [HttpTestingController],
    (_backend: HttpTestingController) => {
      _backend.verify();
    }
  ));

  it('should be created', () => {
    expect(service).toBeTruthy();
  });

  const url = 'http://example.com/users';
  test('should fetch a list of users',done=>{
    service.getList()
    .subscribe(data=>{
      expect(data).toEqual(outputArray)
      done()
    })
    backend.expectOne((req: HttpRequest<any>) => {
        return req.url === url && req.method === 'GET';
      }, `GET all list data from ${url}`)
      .flush(outputArray);
  })
});

Writing Unit Tests for Container Components

Container Components are complex components and often this complexity can lead to confusion towards how to write unit tests for a container component. To avoid this you can take the shallow and deep testing approach of writing unit tests.

You can learn more about this approach here

Sample App container component


@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})

export class AppComponent implements OnInit{
  title = 'my-test-app';
  list$ : Observable<UserModel[]>;
  constructor(
    private listService :ListingService,
  ){
  }
  ngOnInit(){
    this.initListService()
  }
  initListService(){
    this.list$ =  this.listService.getList();
  }
  onClicked(user){

  }
}

Setting up the Container for unit tests

let fixture : ComponentFixture<AppComponent>;
  let service : ListingService;
  let component : AppComponent;
  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [
        AppComponent
      ],
      providers:[
        {provide:ListingService,useClass:MockListService}
      ],
      schemas: [CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA]
    }).compileComponents();
  }));
  beforeEach(()=>{
    fixture = TestBed.createComponent(AppComponent)
    component = fixture.debugElement.componentInstance;
    service = fixture.componentRef.injector.get(ListingService);
    fixture.detectChanges()
  })

Writing Shallow Tests

Unit tests for testing only parts which are isolated from other components in the current container like whether all DOM components written as part of this component's template are being rendered as desired, component is being set up by fetching data from services and the component outputs are working as intended.


  it('should create the app', () => {

    expect(component).toBeTruthy();
  });


  it('should render title in a h1 tag',() => {
    const compiled = fixture.debugElement.nativeElement;
    expect(queryByTestId(compiled,'app-title')).not.toBeNull();
    expect(queryByTestId(compiled,'app-title').textContent).toEqual(component.title)
  });

  test('should fetch the user list from the listing service',async(()=>{
    const spy = jest.spyOn(service,'getList');
    var expectedObservable = cold('-a',{a:outputArray})
    spy.mockReturnValue(expectedObservable)
    component.ngOnInit()
    fixture.detectChanges()
    expect(spy).toHaveBeenCalled();
    expect(component.list$).toBeObservable(expectedObservable)
    getTestScheduler().flush()
    fixture.detectChanges()
    component.list$.subscribe((o)=>{
      fixture.detectChanges()
      var list = fixture.nativeElement.querySelectorAll('app-list-row-component')
      expect(list.length).toEqual(outputArray.length)
    })
    spy.mockRestore()
  }))

Writing Deep Tests

Set of unit tests where the aim is to check the interaction in the component between the child / internal components and the providers and dispatchers attached to the component.


test('should call onClicked when app-list-row-component is clicked',()=>{
    const spy = jest.spyOn(service,'getList');
    var expectedObservable = cold('a',{a:outputArray})
    spy.mockReturnValue(expectedObservable)
    component.initListService()
    getTestScheduler().flush()
    var onClicked = spyOn(component,'onClicked').and.callThrough();
    component.list$.subscribe((o)=>{
      fixture.detectChanges()
      var row0 = fixture.debugElement.query((el)=>{
        return el.properties['data-testid'] === 'row0'
      }).componentInstance as ListRowComponentComponent
      row0.rowClick.emit();
      expect(onClicked).toHaveBeenCalledWith(outputArray[0])
    })
  })

Conclusion

Through this article I hope to have given the reader a brief knowledge of the key concepts required to integrate Unit testing into your Frontend code and also some tips on how to write unit tests for complex components and the way you should design your application so it becomes easy to maintain a healthy codebase.

You can find the entire code for the sample app used in this post here

Please feel free to fork and practice unit testing using this setup.

Discussion

pic
Editor guide
Collapse
joeyhub profile image
Joey Hernández

I've had a few UI developers with an aversion to unit testing. I let them do what they were paid to do, develop UIs. I've never seen a happier more productive bunch of UI developers in my entire career.