fbpx

Lately, we have been using Angular as our main framework for developing single-page applications (SPA) in ORBIT Lab, so in this blogpost I will try to sum up my experiences and hopefully, in the process, create a guide with some examples of the different methods for testing components, services, templates and routes. 

Angular itself has complete documentation on how to build and structure your application in https://angular.io/docs. The documentation is easy to navigate, read and follow. It includes mainly good examples. They have also written a lot about testing https://angular.io/guide/testing, but in my opinion, it was hard to use this documentation directly in an attempt to do TDD (test-driven development) when building a SPA. So here is my attempt to make it easier. 

TDD

This is not a post about TDD, but just to make sure we are all on the same page, TDD is a technique of writing code, where you always start writing a failing test, then write the minimum code that makes this test pass, and lastly refactor your code. So Red, Green, Red, Green. 

This is not all there is to know about TDD, but there are various blogposts about this subject to supplement what I write here. Other resources to learn about TDD could be Martin Fowler https://martinfowler.com/bliki/TestDrivenDevelopment.html or the book Test-Driven Development by Kent Beck. 

Unit tests

In this post, we are going to focus on unit testing and not on integration testing. We will focus on testing components and services in an Angular project. Services are probably the easiest to test, since they have the least interaction with any Angular-specific code, so here we will be starting with an example with services. Afterwards we will look at the different aspects of testing Angular Components. 

Testing Services

To test your services, you will need to mock/stub (mock from here on) out your dependencies. In Angular, this means registering these in your module and, in your test, manually create your service and injecting a mock instead. Creating mocks can be done with Jasmine:

jasmine.createSpyObject<YourDepencyClass>( 

  “YourDepencyClass”, 

  [“methodA”, “methodB”, ..], {} 

)

Since Angular uses Typescript as default, let us make sure we have types in our tests as well. So createSpyObject takes a type argument which is the class you mock. The first normal argument is the baseName, so this is your class name as a string. This is how Jasmine locates the class. The second argument is the list of methods you need to mock and the last parameter, which is optional, is the list of properties you want to mock in the class. 

This way, we can mock any object we are depending on and – as stated earlier – services are quite easy to test, as we normally don’t have any angular-specific behavior to handle. An example of a very straightforward test from our authentication module: 

it("should call repository", async () => { 
  (userRepository as any).signInWithEmailAndPassword.and.callFake( 

    function() { 
      expect(arguments[0]).toEqual(email); 

      expect(arguments[1]).toEqual(password); 
    } 
  ); 
   await service.signIn(email, password); 

   expect(userRepository.signInWithEmailAndPassword) 

.toHaveBeenCalled(); 
});

The second line (userRepository as any).signInWithEmailAndPassword mock out our repository and assert that the argument the function signInWithEmailAndPassword is called with, is correct. As our service should only call the repository with same parameters, this is the only thing we test here. 

Testing in Jasmine is structured in the function it(description, function), which correspond to test cases and can be grouped in describe(description, () => `a number of tests`). ‘expect’ has a number of different methods. Therefore, in the above example, it would be enough to use toHaveBeenCalledWith(email, password), to simplify my userRepository mock. 

This is more or less all there is to testing services in Angular. It is – as stated earlier – straightforward and no harder than testing other ‘normal’ Typescript classes.

Observables 

There is, of course, one extra thing regarding observables in Angular, and to test that the state in the observable is what you expect, you should use subscribe: 

it("should return userId from repository", async (done) => { 
  (repository as any).getCurrentUserUID.and.returnValue(Promise.resolve(UID)); 
 
  service.userUid.subscribe(uid => { 

    expect(uid).toEqual(UID); 
    done(); 
  }); 
 
  service.getCurrentUserUID(); 
});

There are two things to notice here. The assert part is set up before the acting (the invocation of the service). The service.getCurrentUserUID updates an observable in the service class, and this is asserted in the subscribe part. Also, the done() function, which is an optional parameter in test function, and since this test is asynchronous, we need to tell Jasmine when the test is finished. 

Testing Components

Testing components have a bit more to them, since we are testing closer to the Angular framework. Here, we will exemplify this by talking about testing routes, UI, using stubs and mocks and show how they can be used along with Angular. 

But before we go into testing the specific Angular component, we will just show a simple test of a public method on an Angular component. The test itself can be fairly straightforward and in many usecases it looks like testing services and Typescript classes in general:

it("should call Service.getStudy when user is authenticated", async () => { 
  (studyService as any).getStudy.and.returnValue(of(STUDY)); 
  isAuthObservable.next(true); 
  await fixture.detectChanges(); 
 
  expect(studyService.getStudy).toHaveBeenCalledWith(ID); 
});

This test is simple enough, given some changes to the isAuthObservable, the component should then update its state by calling a service. The hard part of starting to test your component lies mostly in setting up the component so that it can actually build. For this specific component, setting up the test module is quite cumbersome:

TestBed.configureTestingModule({ 
  declarations: [ ManageStudyComponent, LoginStubComponent, AddParticipantsStubComponent ], 
  imports: [ 
    RouterTestingModule.withRoutes([ 
      { path: "", component: ManageStudyComponent }, 
      { path: "login", component: LoginStubComponent } 
    ]), 
    MatInputModule 
    MatCardModule 
    MatIconModule, 
        MatDatepickerModule, 
    MatNativeDateModule, 
     MatFormFieldModule, 
    FormsModule 
    ReactiveFormsModule 
    NoopAnimationsModule  ], 
  providers: [ 
    AppRoutingModule, 
      { provide: ActivatedRoute, useValue: { 

        snapshot: { 
        params: { id: ID } 
          } 
        } 
      }, 
    { provide: UserService, useValue: userService }, 
    { provide: LogService, useValue: logService }, 
    { provide: StudyService, useValue: studyService }, 
  ] 
}).compileComponents();

Since this module is using a large number of services, Angular Material design modules, plus routing, creating the component and mocking the services correctly can be a quite time-consuming. I at least find it a lot easier when doing it in a TDD style, because the errors messages when setting up a TestingModule are hard to read. Doing TDD style programming makes it easier to compare changes with previous iterations and thereby, it is a lot easier to figure out what you did not include correctly.  

Navigation

As the last example is testing navigation, this is actually not too difficult, but for us at least it took some time to figure out how to do this. 

Say you have a component that utilizes Angular’s Router class:

constructor( 
   private userService: UserService, 
   private router: Router 
) { }

Testing that some function actually navigates to the correct route is all about using RouterTestingModule. This must be added to your import object when setting up the test module. After which you call TestBed.inject to inject the router module. Then all that is left to do is to mock the router object directly – like below – or testing that the router.navigateByUrl has been called with the correct parameters:

it("should redirect to /login", async () => { 
  const router = TestBed.inject(Router); 
  spyOn(router, "navigateByUrl") 

     .and.returnValue(Promise.resolve(true)); 
 
  await component.logout(); 
 
  expect(router.navigateByUrl).toHaveBeenCalledWith("login"); 
});

Sum-up

This should give a brief overview of the different methods of testing in Angular – at least the test methods we have been using extensively. We have not covered how to test that the UI is calling the components correctly, but other than this, you should be able to start writing unit tests for your Angular project.