DEV Community

Preston Lamb
Preston Lamb

Posted on • Originally published at prestonlamb.com on

Intro to Testing in Angular

I've recently been watching a Pluralsight course on testing in Angular by Joe Eames. I'm gonna be honest here: I've been using Angular since late 2015/early 2016. I've gone from the Angular 2 beta to Angular v7.0. I've written 0 tests. Oops...

But the way I see it is we can always improve, so this is the area where I'm focusing on improving this year. I hope to say that by the end of the year I'm an experienced Angular tester and haven't ignored this crucial part of developing any more.

Joe introduces us to 3 types of tests you can write for your Angular application: isolated tests, shallow integration tests and deep integration tests. Let's review each of these and show an example.

Isolated Tests

An isolated test is exactly what it sounds like: it tests a piece of code completely isolated from other parts of the code base. As an example, let's say you have a service:

@Injectable()
export class ToasterService() {
    addMessage(msg: string) {
        this.messages.push({ msg })
    }
    ...
}
Enter fullscreen mode Exit fullscreen mode

Now, you may want to write a test that confirms that the addMessage function works properly. You don't want to make sure that it's called correctly from a component, or anything like that; you just want to make sure that when it's called the proper information is returned or the supplied parameters are handled correctly.

We could write an isolated test for this in this manner:

import { ToasterService } from './toaster.service';

describe('ToasterService', () => {
    let service: ToasterService;

    it('should have no messages to start', () => {
        service = new ToasterService();
        expect(service.messages.length).toBe(0);
    });

    it('should add a message when add is called', () => {
        service = new ToasterService();

        service.add('message 1');

        expect(service.messages.length).toBe(1);
    });

    it('should remove all messages when clear is called', () => {
        service = new ToasterService();
        service.add('message 1');

        service.clear();

        expect(service.messages.length).toBe(0);
    });
});
Enter fullscreen mode Exit fullscreen mode

This will allow us to see if messages are added to the array like they should be, and/or removed, or whatever other action we may need to perform.

Shallow Tests

The next type of test is a shallow test. This type of test, at least in part, will test out functionality at the component level. It won't test out child components or services. It's limited to what's actually in that single component. To do this, we'll need to use mock services to fake the return information.

Here's a full example of a shallow test:

describe('ToastersComponent (Shallow)', () => {
    let fixture: ComponentFixture;
    let mockToasterService;
    let MESSAGES;

    @Component({
        selector: 'app-toast',
        template: '',
    })
    class MockToastComponent {
        @Input() hero: Hero;
    }

    beforeEach(() => {
        MESSAGES = [
            { msg: 'Warning Message', level: 'warning' },
            { msg: 'Error Message', level: 'error' },
            { msg: 'Success Message', level: 'success' },
        ];
        mockToasterService = jasmine.createSpyObj(['getMessages', 'addMessage', 'deleteMessage']);

        TestBed.configureTestingModule({
            declarations: [ToastersComponent, MockToastComponent],
            providers: [{ provide: ToasterService, useValue: mockToasterService }],
        });
        fixture = TestBed.createComponent(ToastersComponent);
    });

    it('should correctly set the messages property from the service', () => {
        mockToasterService.getMessages.and.returnValue(of(MESSAGES));
        fixture.detectChanges();

        expect(fixture.componentInstance.messages.length).toBe(3);
    });

    it('should create one li for each toast', () => {
        mockToasterService.getMessages.and.returnValue(of(MESSAGES));
        fixture.detectChanges();

        expect(fixture.debugElement.queryAll(By.css('li')).length).toBe(3);
    });
});
Enter fullscreen mode Exit fullscreen mode

As a brief overview of what happens here, before each test we reset the messages array, prepare the testing module, and a test component to use. Then we test that the messages come back from the server and that the component then lists them out on the screen.

Deep Tests

The third and last in this article is a deep test. This test will test functionality in the component as well as making sure that its child components display information properly as well.

Here's an example of that:

describe('ToastersComponent (Deep)', () => {
    let fixture: ComponentFixture;
    let mockToasterService;
    let MESSAGES;

    beforeEach(() => {
        MESSAGES = [
            { id: 1, name: 'SpiderDude', strength: 8 },
            { id: 2, name: 'Wonderful Woman', strength: 24 },
            { id: 3, name: 'SuperDude', strength: 55 },
        ];
        mockToasterService = jasmine.createSpyObj(['getMessages', 'addMessage', 'deleteMessage']);

        TestBed.configureTestingModule({
            declarations: [ToastersComponent, ToastComponent],
            providers: [{ provide: ToasterService, useValue: mockToasterService }],
            schemas: [NO_ERRORS_SCHEMA],
        });
        fixture = TestBed.createComponent(ToastersComponent);
    });

    it('should render each toast as a toast component', () => {
        mockToasterService.getMessages.and.returnValue(of(MESSAGES));

        fixture.detectChanges();

        const toastComponentDEs = fixture.debugElement.queryAll(By.directive(ToastComponent));

        expect(toastComponentDEs.length).toBe(3);
        for (let i = 0; i < toastComponentDEs.length; i++) {
            expect(toastComponentDEs[i].componentInstance.toast).toEqual(MESSAGES[i]);
        }
    });

    it(`should call ToasterService.deleteMessage when the ToastComponent's delete button is clicked`, () => {
        spyOn(fixture.componentInstance, 'delete');
        mockToasterService.getMessages.and.returnValue(of(MESSAGES));

        fixture.detectChanges();

        const toastComponents = fixture.debugElement.queryAll(By.directive(ToastComponent));
        // Click the actual HTML button
        // toastComponents[0].query(By.css('button')).triggerEventHandler('click', { stopPropagation: () => {} });
        // raise the event manually
        // (toastComponents[0].componentInstance).delete.emit(undefined);
        // trigger the event using the debugElement
        toastComponents[0].triggerEventHandler('delete', null);

        expect(fixture.componentInstance.delete).toHaveBeenCalledWith(MESSAGES[0]);
    });
});
Enter fullscreen mode Exit fullscreen mode

This is very similar to the shallow tests, with the exception that we reach down into children components and test those as well. We can test actually clicking the button, or triggering events programmatically, and then we can see what happens. In short, we can test the full functionality of the components to ensure that, when changes are made, the application is not broken.

Conclusion

These three types of tests will cover all aspects of your Angular application. You may not need all three types of tests for each part of your application, but this will give you the tools to do so when needed.

I'm excited to add these tests to my applications. The security and peace of mind that will come from having tests on components and services will be well worth the extra time it will take to write the tests.

If you have any tips or tricks, I'd love to hear about them! I'm going to write a follow up article after actually implementing this to talk about pit falls and things I learned along the way!

Top comments (0)