mardi 7 juin 2016

Testing component logic with Angular2 TestComponentBuilder

There are a lot of different approaches to unit test your angular application you can find at the moment. A lot are already outdated and basically there's no real documentation at this point. So im really not sure which approach to use.

It seems a good approach at the moment is to use TestComponentBuilder, but i have some trouble to test parts of my code especially if a function on my component uses an injected service which returns an observable.

For example a basic Login Component with a Authentication Service (which uses a BackendService for the requests). I leave out the templates here, because i don't want to test them with UnitTests (as far as i understood, TestComponentBuilder is pretty useful for this, but i just want to use a common approach for all my unit tests, and the it seems that TestComponentBuilder is supposed to handle every testable aspect, please correct me if i'm wrong here)

So i got my LoginComponent:

export class LoginComponent {
    user:User;
    isLoggingIn:boolean;
    errorMessage:string;

    username:string;
    password:string;

    constructor(private _authService:AuthService, private _router:Router) {
        this._authService.isLoggedIn().subscribe(isLoggedIn => {
            if(isLoggedIn) {
                this._router.navigateByUrl('/anotherView');
            }
        });
    }

    login():any {
        this.errorMessage = null;
        this.isLoggingIn = true;
        this._authService.login(this.username, this.password)
            .subscribe(
                user => {
                    this.user = user;
                    setTimeout(() => {
                        this._router.navigateByUrl('/anotherView');
                    }, 2000);
                },
                errorMessage => {
                    this.password = '';
                    this.errorMessage = errorMessage;
                    this.isLoggingIn = false;
                }
            );
    }
}

My AuthService:

@Injectable()
export class AuthService {

    private _user:User;
    private _urls:any = {
        ...
    };

    constructor( private _backendService:BackendService,
                 @Inject(APP_CONFIG) private _config:Config,
                 private _localStorage:LocalstorageService,
                 private _router:Router) {
        this._user = _localStorage.get(LOCALSTORAGE_KEYS.CURRENT_USER);
    }

    get user():User {
        return this._user || this._localStorage.get(LOCALSTORAGE_KEYS.CURRENT_USER);
    }

    set user(user:User) {
        this._user = user;
        if (user) {
            this._localStorage.set(LOCALSTORAGE_KEYS.CURRENT_USER, user);
        } else {
            this._localStorage.remove(LOCALSTORAGE_KEYS.CURRENT_USER);
        }
    }

    isLoggedIn (): Observable<boolean> {
        return this._backendService.get(this._config.apiUrl + this._urls.isLoggedIn)
            .map(response => {
                return !(!response || !response.IsUserAuthenticated);
            });
    }

    login (username:string, password:string): Observable<User> {
        let body = JSON.stringify({username, password});

        return this._backendService.post(this._config.apiUrl + this._urls.login, body)
            .map(() => {
                this.user = new User(username);
                return this.user;
            });
    }

    logout ():Observable<any> {
        return this._backendService.get(this._config.apiUrl + this._urls.logout)
            .map(() => {
                this.user = null;
                this._router.navigateByUrl('/login');
                return true;
            });
    }
}

and finally my BackendService:

@Injectable()
export class BackendService {
    _lastErrorCode:number;

    private _errorCodes = {
        ...
    };

    constructor( private _http:Http, private _router:Router) {
    }

    post(url:string, body:any):Observable<any> {
        let options = new RequestOptions();

        this._lastErrorCode = 0;

        return this._http.post(url, body, options)
            .map((response:any) => {

                ...

                return body.Data;
            })
            .catch(this._handleError);
    }

    ...  

    private _handleError(error:any) {

        ...

        let errMsg = error.message || 'Server error';
        return Observable.throw(errMsg);
    }
}

Now i want to test the basic logic of logging in, one time it should fail and i expect an error message (which is thrown by my BackendService in its handleError function) and in another test it should login and set my User-object

This is my current approach for my Login.component.spec:

export function main() {
    describe('Login', () => {

        beforeEachProviders(() => [
            ROUTER_FAKE_PROVIDERS
        ]);

        it('should try and fail logging in',
            inject([TestComponentBuilder], (tcb: TestComponentBuilder) => {
                tcb.createAsync(TestComponent)
                    .then((fixture: any) => {
                        fixture.detectChanges();
                        let loginInstance = fixture.debugElement.children[0].componentInstance;

                        expect(loginInstance.errorMessage).toBeUndefined();

                        loginInstance.login();
                        fixture.detectChanges();
                        expect(loginInstance.isLoggingIn).toBe(true);

                        fixture.detectChanges();
                        expect(loginInstance.isLoggingIn).toBe(false);
                        expect(loginInstance.errorMessage.length).toBeGreaterThan(0);
                    });
            }));

        it('should log in',
            inject([TestComponentBuilder], (tcb: TestComponentBuilder) => {
                tcb.createAsync(TestComponent)
                    .then((fixture: any) => {
                        fixture.detectChanges();
                        let loginInstance = fixture.debugElement.children[0].componentInstance;

                        loginInstance.username = 'abc';
                        loginInstance.password = '123';

                        loginInstance.login();
                        fixture.detectChanges();
                        expect(loginInstance.isLoggingIn).toBe(true);

                        fixture.detectChanges();
                        expect(loginInstance.isLoggingIn).toBe(true);
                        expect(loginInstance.user).toEqual(jasmine.any(User));
                    });
            }));

    });

}

@Component({
    selector: 'test-cmp',
    template: `<my-login></my-login>`,
    directives: [LoginComponent],
    providers: [
        HTTP_PROVIDERS,
        provide(APP_CONFIG, {useValue: CONFIG}),
        LocalstorageService,
        BackendService,
        AuthService,
        BaseRequestOptions,
        MockBackend,
        provide(Http, {
            useFactory: function(backend:ConnectionBackend, defaultOptions:BaseRequestOptions) {
                return new Http(backend, defaultOptions);
            },
            deps: [MockBackend, BaseRequestOptions]
        })
    ]
})
class TestComponent {
}

There are several issues with this test.

  • ERROR: 'Unhandled Promise rejection:', 'Cannot read property 'length' of null' I get this for the test of `loginInstance.errorMessage.length
  • Expected true to be false. in the first test after i called login
  • Expected undefined to equal <jasmine.any(User)>. in the second test after it should have logged in.

Any hints how to solve this? Am i using a wrong approach here? Any help would be really appreciated (and im sorry for the wall of text / code ;) )

Aucun commentaire:

Enregistrer un commentaire