使用Angular2 TestComponentBuilder测试组件逻辑

时间:2016-06-07 09:23:50

标签: unit-testing testing angular jasmine karma-runner

目前您可以找到很多不同的方法来对您的角度应用进行单元测试。很多已经过时了,基本上目前还没有真正的文档。所以我真的不确定使用哪种方法。

目前似乎一个好的方法是使用TestComponentBuilder,但是我在测试部分代码时遇到了一些麻烦,特别是如果我的组件上的函数使用了一个返回可观察的注入服务。

例如,具有身份验证服务的基本登录组件(对请求使用BackendService)。 我在这里省略了模板,因为我不想用UnitTests测试它们(据我所知,TestComponentBuilder对此非常有用,但我只想对我的所有单元测试使用通用方法,似乎TestComponentBuilder应该处理每个可测试的方面,请纠正我,如果我在这里错了)

所以我得到了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;
                }
            );
    }
}

我的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;
            });
    }
}

最后我的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);
    }
}

现在我想测试登录的基本逻辑,有一次它应该失败并且我期望一条错误消息(由BackendService函数中的handleError抛出)和另一个测试它应该登录并设置我的User - 对象

这是我目前针对Login.component.spec的方法:

已更新:已添加fakeAsync,如Günters答案所示。

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

        beforeEachProviders(() => [
            ROUTER_FAKE_PROVIDERS
        ]);

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

                        expect(loginInstance.errorMessage).toBeUndefined();

                        loginInstance.login();
                        tick();
                        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], fakeAsync((tcb: TestComponentBuilder) => {
                tcb.createAsync(TestComponent)
                    .then((fixture: any) => {
                        tick();
                        fixture.detectChanges();
                        let loginInstance = fixture.debugElement.children[0].componentInstance;

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

                        loginInstance.login();

                        tick();
                        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 {
}

此测试存在几个问题。

  • ERROR: 'Unhandled Promise rejection:', 'Cannot read property 'length' of null'我得到这个来测试`loginInstance.errorMessage.length
  • 在我致电Expected true to be false. 后的第一次测试中,
  • login
  • Expected undefined to equal <jasmine.any(User)>.在应该登录后的第二次测试中。

任何提示如何解决这个问题?我在这里使用了错误的方法吗? 任何帮助都会非常感激(我很抱歉文本/代码的墙;))

1 个答案:

答案 0 :(得分:1)

由于您无法知道实际调用this._authService.login(this.username, this.password).subscribe( ... )的时间,因此您无法同步继续测试并假设已发生subscribe回调。实际上它还没有发生,因为同步代码(你的测试)首先被执行到最后。

  • 您可以添加人工延迟(丑陋和片状)
  • 您可以在组件中提供可观察或承诺,当您要测试的内容实际完成时发出/解决(丑陋,因为测试代码已添加到生产代码中)
  • 我想最好的选择是使用fakeAsync,它可以更好地控制测试期间的异步执行(我自己也没有使用过它)。
  • 据我所知,在使用zone的Angular测试中会有支持,等待异步队列在测试继续之前变空(我也不知道有关这方面的详细信息)。