If you missed Part 1 and Part 2 of our Testing Angular 2 Apps series, I encourage you to take a look at them.
Note: Article is based on Angular 2 beta.3
Up-to-date Gist with examples for whole series is here: Angular 2 testing gist
Making tests work with Router
The first real problem you’ll meet when trying to test your real app will be the Router. What’s wrong with it? Let’s try to simply add a RouteConfig
to the App component:
import { Component } from 'angular2/core'
import { RouteConfig } from 'angular2/router'
import { TestComponent } from './components/test'
@Component({
selector: 'app',
template: '',
})
@RouteConfig([{ path: '/', component: TestComponent }])
export class App {}
Our simplest test would look like this:
describe('App', () => {
it(
'should be able to test',
injectAsync([TestComponentBuilder], (tcb: TestComponentBuilder) => {
return tcb.createAsync(App).then(componentFixture => {
componentFixture.detectChanges()
expect(true).toBe(true)
})
})
)
})
Note: do you remember there should be added setBaseTestProviders
to make tests work in a browser?
It’s all fine! But the problem is that RouteConfig
just sets properties and doesn’t make our component work as expected. We have to add place where component for given path will be rendered. There’s directive for that called RouterOutlet
. And now the party begins. Here it goes:
import { Component } from 'angular2/core'
import { RouteConfig, RouterOutlet } from 'angular2/router'
import { TestComponent } from './components/test'
@Component({
selector: 'app',
template: '<router-outlet></router-outlet>',
directives: [RouterOutlet],
})
@RouteConfig([{ path: '/', component: TestComponent }])
export class App {}
And what happens now? Yep, it fails:
Failed: No provider for Router! (RouterOutlet -> Router)
So let’s add a Router
:
beforeEachProviders(() => [provide(Router)])
You’ll end up with such a happy error:
Failed: Cannot resolve all parameters for 'Router'(?, ?, ?).
The way to solve it is to use RootRouter
when app asks about Router
:
beforeEachProviders(() => [provide(Router, { useClass: RootRouter })])
But now it still fails:
Failed: No provider for RouteRegistry! (RouterOutlet -> Router -> RouteRegistry)
This is because RootRouter
requires 3 parameters to be injected:
registry
location
primaryComponent
In case of tests we can provide them using SpyLocation
mock for Location
provider and App
as ROUTER_PRIMARY_COMPONENT
. Providers now should look like these:
beforeEachProviders(() => [
RouteRegistry,
provide(Location, { useClass: SpyLocation }),
provide(ROUTER_PRIMARY_COMPONENT, { useValue: App }),
provide(Router, { useClass: RootRouter }),
])
Bingo! Now we do have working Router injection. It should fix tests when you’re using router in your application.
Note that we’re not talking about testing routes, url changes etc. but just adding required dependencies for routerOutlet
.
Http in services
But let’s move forward to another topic. Do you remember how to test services? Now we need them again.
Common pattern is to keep connection with backend outside the component and simply not to inject http
directly into the component. The right place seems to be a service. So let’s create one:
import {Injectable} from 'angular2/core';
import {Http} from 'angular2/http';
@Injectable()
export class TestService {
constructor(private http: Http) {}
getUsers() {
return this.http.get('http://foo.bar');
}
}
If you watch closely you’ve probably just realised I added something what I was not talking about before, right? It’s Injectable
decorator. Why do we need this now? Before I said that services are pure JS classes without any special decorator needed. That’s true unless you’d like to inject something into a service. Why is that? Because Angular 2 makes dependency injections by looking for class’ metadata. These metadata are being added when you add some decorator like @Component
or @Directive
but in case of services we don’t need anything like this. So to tell Angular we want to inject something we can add just @Injectable
.
Note: It seems to be a good practice to add @Injectable
for every service (even when you don’t need to inject anything) but it’s crucial to understand why it's here.
I also injected Http
. Are you familiar with $http
from AngularJS? That one was using Promises and it worked fine but the new one is built on the top of RxJS. And it’s extremely powerful tool.
A encourage you to take a look at the rx-book. Keep in mind Angular uses v5.0 which is in beta now.
Let’s test this service. Some imports will be handy:
import {
BaseRequestOptions,
Response,
ResponseOptions,
Http,
} from 'angular2/http'
import { MockBackend, MockConnection } from 'angular2/http/testing'
Now we can inject it into test cases:
beforeEachProviders(() => [
TestService,
BaseRequestOptions,
MockBackend,
provide(Http, {
useFactory: (backend: MockBackend, defaultOptions: BaseRequestOptions) => {
return new Http(backend, defaultOptions)
},
deps: [MockBackend, BaseRequestOptions],
}),
])
One thing to notice is the Http
mock. Each time when something needs the Http
we are using our concrete implementation. Moreover we are injecting dependencies into the factory. It’s worth to add that in case factory returns instance of class it won’t be a singleton.
With mocked Http
there’s one thing left before the actual test. I’ve mocked all http calls to return simple string:
beforeEach(inject([MockBackend], (backend: MockBackend) => {
const baseResponse = new Response(
new ResponseOptions({ body: 'got response' })
)
backend.connections.subscribe((c: MockConnection) =>
c.mockRespond(baseResponse)
)
}))
And there you go! Finally we can create our test case:
it('should return response when subscribed to getUsers', inject(
[TestService],
(testService: TestService) => {
testService.getUsers().subscribe((res: Response) => {
expect(res.text()).toBe('got response')
})
}
))
Now it’s your turn to play, especially with MockConnection
and RxJS
operators. It should give you the kickstart to the Angular 2 testing and understand what’s really going on there. Now you can take a look at Angular core tests and look there for solutions to specific problems.
If there’s anything I missed or you’d like to discuss some solution please leave your thoughs in comments.