DEV Community

Jordan Powell for Angular

Posted on • Updated on

Unit Testing in Angular - To TestBed or NOT to TestBed

I recently started consulting for a new client (no names please). As I began to create a new feature and write unit tests I noticed several things. First that writing tests were more difficult than necessary (I'll get into this more specifically later) and that the Test runner was running very slowly.

As I began to look deeper into the tests I noticed a difference between my unit tests and the previously written tests from other parts in the app. I discovered that I was using TestBed to create my tests. This wasn't the case anywhere else in the app. I found this to be very interesting as I've always used TestBed in the past and performance was not an issue.

This led me to do some more research on the topic and see if any others in the Angular Community were not using TestBed. I couldn't find many articles but was able to find an episode of The Angular Show podcast where Joe Eames and Shai Reznik were having a very healthy debate on why you should or shouldn't use TestBed. I won't spoil the episode for you but I will admit that for someone who works in Angular every day this was the first I had ever heard a case (and a good one at that) for not using TestBed.

Though I was still skeptical, I figured I would give it a shot on this project and see if it made a difference. I was quickly blown away by the increase in performance this approach brought me. This led me to ask the question of why...which ultimately led to this blog article.

Performance

When you remove TestBed from your component spec files it essentially no longer tests the DOM. It now only tests the component class itself. This felt like a code smell at first but ultimately the more I thought about it, the more I realized that a true unit test should only be testing one unit of code. How the component's HTML template interacted with its component class really becomes an integration test, testing the integration between the two.

So let me unpack this a little bit more. When you use the Angular CLI and generate a new component ng g c my-feature it will render the following files:

  • my-feature.component.html
  • my-feature.component.scss
  • my-feature.component.ts
  • my-feature.component.spec.ts

When you open up the my-feature.component.spec.ts file we see the following:

import { async, ComponentFixture, TestBed } from '@angular/core/testing';

import { MyFeatureComponent } from './my-feature.component';

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

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

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

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

This essentially before each test will create a new instance of the MyFeatureComponent class and the DOM. This example is trivial but in an application with hundreds of components, generating the DOM for every test can become costly.

WITHOUT TestBed

import { async, ComponentFixture, TestBed } from '@angular/core/testing';

import { MyFeatureComponent } from './my-feature.component';

describe('MyFeatureComponent', () => {
  let component: MyFeatureComponent;

  beforeEach(() => {
    component = new MyFeatureComponent()
  });

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

By just newing up the MyFeatureComponent class before each test it will just create the class instance and forgo the DOM itself.

What about Dependencies?

Let's say our component now has 2 dependencies. One to a UserService and another to a MyFeatureService. How do we handle writing tests that need dependencies provided?

WITH TestBed

@angular/core/testing';

import { MyFeatureComponent } from './my-feature.component';
import { UserService, MyFeatureService } from 'src/app/services';

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

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [ MyFeatureComponent ],
      providers: [UserService, MyFeatureService]
    })
    .compileComponents();
  }));

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

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

WTHOUT TestBed

import { async, ComponentFixture, TestBed } from '@angular/core/testing';

import { MyFeatureComponent } from './my-feature.component';
import { UserService, MyFeatureService } from 'src/app/services';

describe('MyFeatureComponent', () => {
  let component: MyFeatureComponent;
  const userService = new UserService();
  const myFeatureService = new MyFeatureService();

  beforeEach(() => {
    component = new MyFeatureComponent(userService, myFeatureService);
  });

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

*** Note: The order of dependencies you add into the new Component class instance does need to be in the correct order with this approach.

What if my dependencies have dependencies?

I know you were probably thinking the same thing when looking at the previous example as most dependencies have other dependencies. For example, a service typically has a dependency upon HttpClient which enables it to make network requests to an API. When this happens (which is almost always) we typically use a mock or a fake.

WITH TestBed

@angular/core/testing';

import { MyFeatureComponent } from './my-feature.component';
import { UserService, MyFeatureService } from 'src/app/services';

class FakeMyFeatureService {

}

class FakeUserService {

}

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

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [ MyFeatureComponent ],
      providers: [
        { provide: UserService, useClass: FakeUserService },
        { provide: MyFeatureService, useClass: FakeMyFeatureService }
      ]
    })
    .compileComponents();
  }));

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

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

WITHOUT TestBed

import { async, ComponentFixture, TestBed } from '@angular/core/testing';

import { MyFeatureComponent } from './my-feature.component';
import { UserService, MyFeatureService } from 'src/app/services';

class FakeMyFeatureService {

}

class FakeUserService {

}

describe('MyFeatureComponent', () => {
  let component: MyFeatureComponent;
  const userService = new FakeUserService();
  const myFeatureService = new FakeMyFeatureService();

  beforeEach(() => {
    component = new MyFeatureComponent(userService, myFeatureService);
  });

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

*** Note: You will want to use spies on those dependencies to actually test the parts of your component you care about.

Less Flaky Tests

Without TestBed, we are no longer testing the DOM itself which means that changes to the DOM will no longer break your tests. I mean how many times have you created a component somewhere in your Angular application all of a sudden tests start failing? This is because TestBed is creating the DOM beforeEach test. When a component and its dependencies are added its parent component will now fail.

Let's take a look at this more in-depth by creating a parent component called MyParentComponent with ng g c my-parent

Now let's take a look at the my-parent.component.spec.ts file:

WITH TestBed

@angular/core/testing';

import { MyParentComponent } from './my-parent.component';

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

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

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

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

WITHOUT TestBed

import { async, ComponentFixture, TestBed } from '@angular/core/testing';

import { MyParentComponent } from './my-parent.component';

describe('MyParentComponent', () => {
  let component: MyParentComponent;

  beforeEach(() => {
    component = new MyParentComponent();
  });

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

Now let's add MyFeatureComponent to the template as a child of MyParentComponent.

<my-parent>
  <my-feature />
</my-parent>
Enter fullscreen mode Exit fullscreen mode

In this example, my-parent.component.spec.ts tests are now all failing as it doesn't have a declaration for MyFeatureComponent or it's providers UserService and MyFeatureService. Below is now what we need to do to get those tests back up and passing.

WITH TestBed

@angular/core/testing';

import { MyParentComponent } from './my-parent.component';
import { MyFeatureComponent } from './my-feature/my-feature.component';
import { UserService, MyFeatureService } from 'src/app/services';

class FakeMyFeatureService {

}

class FakeUserService {

}

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

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [ MyParentComponent, MyFeatureComponent ],
      providers: [
        { provide: UserService, useClass: FakeUserService },
        { provide: MyFeatureService, useClass: FakeMyFeatureService }
      ]
    })
    .compileComponents();
  }));

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

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

WITHOUT TestBed

Thank You
This requires no changes as changes to the template had no effect on the test suite!

Other Things To Consider

There are some tradeoffs we need to consider by not testing any part of the DOM. The biggest being that we are no longer testing the DOM or the integration between it and it's component class. In most cases, we don't particularly care that when a button is clicked we test that it calls a method on its component class. We tend to trust Angular's (click) event binding to just work. Therefore we mostly care that the method it calls actually works as expected. HOWEVER, because we are no longer testing this integration we no longer have the assurance that another developer on the team accidentally deletes that integration. Or that after refactoring that this particular button calls this specific method.

I do believe this can be a relatively small tradeoff and that this sort of test can be handled more appropriately using e2e tests. I would also mention that this is not an all or nothing approach to testing. In the instances in your application where you do want to test the integration between the template and its class, you can still use TestBed. You essentially just no longer get the benefits above for the parts that are now using TestBed.

Note: In this example the Angular app was running on Angular version 7. Angular 9 and later now render your applications using IVY which released with some performance improvements for TestBed.

Conclusion

As you can see from our trivial example, that by removing TestBed from our Angular components spec files we are able to improve the performance of our test runner and are able to remove some of the flakiness. Of course, the magnitude by which your test speed will improve will depend upon the size of your application and the way your application is built. Applications with very large components (which is a bigger code smell) will benefit the most from this approach. Ultimately the biggest benefit to writing tests without TestBed is that you are truly writing unit tests that should be easy to write, more reliable, and provide very quick feedback. The easier, more reliable, and quicker feedback you can get from writing tests the more you can leverage the benefits of unit tests.

Top comments (11)

Collapse
 
layzee profile image
Lars Gyrup Brink Nielsen • Edited

Which version of Angular are you using in the system you saw the speed differences in? With or without Ivy? A big component testing (TestBed) optimization was introduced in Angular Ivy version 9.

We don't have to render the full DOM of every component. We can use shallow component tests where we don't render the child components, but instead focus on the DOM generated by a single component template.

Collapse
 
jordanpowell88 profile image
Jordan Powell

Yes. I did forget to mention this in my article but we are currently on 7 and therefore not able to take advantage of the benefits of that optimization in IVY.

Collapse
 
layzee profile image
Lars Gyrup Brink Nielsen

That's sad. Angular 7 has been end of life for a while. Even version 8 will receive no more patches two months from now.

Definitely worth mentioning in your article that this might only apply to legacy versions of Angular using View Engine.

Thread Thread
 
jordanpowell88 profile image
Jordan Powell

Yes I am in the process of upgrading the app to 7. And yes I will work on adding that caveat to the article. I had it written down in my outline and then just completely forgot to actually add it into the article. I appreciate the feedback and the reminder!

Collapse
 
stealthmusic profile image
Jan Wedel

We are also on that same path. We were used to writing tests with TestBed and then started to refactor all of them to real unit test, putting more emphasis on integration tests with Cypress and backend mocks.

TestBed was just to slow and adding all modules for dependencies was really tedious.

It’s interesting though, that there is a speed up to be expected with Angular 9.

Collapse
 
jordanpowell88 profile image
Jordan Powell

Their is a noticeable speed improvement with IVY (version 9+). It is nice to have the option to use TestBed when you want to but the main point of the article was that I don’t believe it is necessary in almost all use cases and in the end isn’t then truly a unit test

Collapse
 
flamesoff profile image
Artem

Testing with TestBed is NOT unit, it's Integrational. It's a different level of testing.
And yes, after many years of development I came to conclusion that testing with the TestBed is a pure waste of time.
In the same time unit tests are complete opposite, unit tests are crucial, helpful and easy to maintain.

Collapse
 
martinspire profile image
Martin Spierings

I recently started in a team that is all-in on blackbox testing. So there's no component logic in my tests, just the DOM and everything you do is interacting with the DOM. This means that you need testbed, but that also means that for testing, you don't really care what happens in the controller because ultimately it doesn't matter. It does make it a bit more difficult to simply test certain functions but if it never modifies the DOM do you really need to have it in your component?

We also use NG-Spectator and NG-Mocks to do stubbing of children, services and what have ya which makes it a lot easier to write tests. I really hope the Angular team considers using these modules (or their logic) to upgrade Testbed at some point in the future because its mighty handy. I also prefer it over the use of TypeMoq (as its much easier to read imo).

Without testbed, as you demonstrated, your DOM could be anything and you aren't really testing components. Its great for testing services, but terrible to check whether what the user sees is what you developed. The HTML is still part of what makes angular tick and if you skip that part your tests aren't going to be reliable

Collapse
 
mrgrigri profile image
Michael Richins

At my work we've adopted TypeMoq with out the use of TestBed and it's been amazing.

Collapse
 
joellau profile image
Joel Lau • Edited

great article and interesting discussions in the comments!

does anybody have any thoughts / experiences on angular-testing-library?

Collapse
 
razorblade446 profile image
Frederic Yesid Peña Sánchez

I think is not accurate to say that TestBed is a waste of time, moreover when an Angular Component is strongly tied to HTML.

If you have logic in your component that does not need to interact with HTML, I would suggest that logic to go in a Service.

Also, a good separation in Dumb and Smart components will improve TestBed tests.