Skip to content

Instantly share code, notes, and snippets.

@vsavkin
Created October 6, 2017 18:42
Show Gist options
  • Select an option

  • Save vsavkin/26da931641397be202c35d99f21f6518 to your computer and use it in GitHub Desktop.

Select an option

Save vsavkin/26da931641397be202c35d99f21f6518 to your computer and use it in GitHub Desktop.

Two Types of Tests

As I mentioned in a few places, there are two ways test router-related code. I call them "unit testing" and "integration testing". But because these two terms are used in so many contexts, read comments here to see what I mean:

Essentially, the latter one calls 'router.navigate'. In my experience, Googlers mostly care about integration testing.

Unit Testing

Router does a very poor job at providing support for isolated using testing. Mainly, cause it is super difficult to create ActivatedRoute and RouterState data structures. If we provide some helpers to make their creation easier, we can simplify everything very nicely.

Option 1 (What you suggested)

Simple case:

createActivatedRoute({
    params: {x: "1"},
    queryParams: {y: "2"},
    data: { b: 100 },
    children: [
        { params: {x: "1"}, data: { b: 200 } }
    ]
})

Values changing over time:

createActivatedRoute(cold('a--b|',
    a: {
        params: {x: "1"},
        queryParams: {y: "2"},
        data: { b: 100 },
        children: [
            { params: {x: "1"}, data: { b: 200 } }
        ]
    },
    b: {
        params: {x: "1"},
        queryParams: {y: "2"},
        data: { b: 100 },
        children: [
            { params: {x: "1"}, data: { b: 200 } }
        ]
    }
))

PRO:

  • Resolvers are handled nicely, just via data
  • Very direct

CONS:

  • need to manually sync URL and params (in the example above, the URL is missing!)
  • everything has to be a string, maybe confusing (need to write checks)
  • router.routeConfig won't work cause there is no config

Option 2 (What I suggested)

Simple case:

createActivatedRoute('/one/two?y=2', {
    path: 'one',
    data: {b: 100},
    children: [{path: 'two', data: {b: 200}]
});

Values changing over time (without resolvers):

createActivatedRoute(
    cold('a--b|', {a: '/one/two?y=2', b: '/one/two?y=3'}),
    {
        path: 'one',
        data: {b: 100},
        children: [{path: 'two', data: {b: 200}]
    }
);

Values changing over time (with resolvers):

createActivatedRoute(
    cold('a--b|',
        {
            a: {url: '/one/two?y=2', resolve: {B_ONE: 100, B_TWO: 200}},
            b: {url: '/one/two?y=3', resolve: {B_ONE: 2222, B_TWO: 5555}}
        }
    ),
    {
        path: 'one',
        resolve: {b: 'B_ONE'},
        children: [{path: 'two', resolve: {b: 'B_TWO'}]
    }
);

PRO:

  • Closer to what production code does. Can be used by itself and with production configuration.
  • Terse
  • Everything is in sync: url, params, etc

CONS:

  • Indirect
  • Resolvers are weird (and that's because they cannot be represented in the URL).

Which One?

I thought about them for some time, and I think I prefer your suggestion. I like how direct it is. I think being direct is better than being terse.

Integration Testing

I cover why the router already supports integration testing well here:

I also put together an example showing how to remove all the boilerplate with a 10-line helper here:

Suggestion

I don't think we should use the helper I put together in the repo: it hides TestBed and I'm sure folks will be confused. What I suggest is even smaller, just add this helperl

// provided automically
@Component({
  selector: 'container',
  template: `<router-outlet></router-outlet>`
})
class Container {}

class RouterTestingModule {
    static navigate(url: string, component: Type): Promise<ComponentFixture<any>> {
        TestBed.configureTestBed({
            declarations: [Container]
        }).compileComponents();

        const c = TestBed.createComponent(Container);
        c.detectChanges();

        return TestBed.get(Router).navigateByUrl(url).then(() => {
            c.detectChanges();
            return findComponent(c, component);
        });
    }
}

This is how we can use it:

@NgModule({
    declarations: [OneComponent],
    imports: [
        RouterModule.forChild([
            {path: 'one', component: OneComponent}
        ])
    ]
})
class MyAppModule {
}
TestBed.configureTestingModule({
  imports: [
    MyAppModule,
    RouterTestingModule
  ],
}).compileComponents();

const c = await RouterTestingModule.navigate('one', OneComponent);
expect(c.nativeElement.innertHTML).toContain('one');

We can also remove the promise and the await, if we use fakeAsync.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment