【问题标题】:Testing component logic with Angular2 TestComponentBuilder使用 Angular2 TestComponentBuilder 测试组件逻辑
【发布时间】:2016-06-07 09:23:50
【问题描述】:

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

目前似乎一个好方法是使用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-object

这是我目前对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 &lt;jasmine.any(User)&gt;. 在第二次测试后应该已经登录了。

任何提示如何解决这个问题?我在这里使用错误的方法吗? 任何帮助将不胜感激(我很抱歉文字/代码墙;))

【问题讨论】:

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


    【解决方案1】:

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

    • 您可以添加人为延迟(丑陋且不稳定)
    • 您可以在组件中提供 observables 或 Promise,当您想要测试的事情实际完成时发出/解析(因为测试代码添加到生产代码中而很难看)
    • 我想最好的选择是使用fakeAsync,它可以在测试期间提供对异步执行的更多控制(我自己没有使用过)
    • 据我所知,Angular 测试中会支持使用 zone,在测试继续之前等待异步队列变空(我也不知道这方面的详细信息)。

    【讨论】:

    • 所以我现在尝试了这个,使用fakeAsync,但它没有任何区别。我用我的调整更新了我上面的问题。但结果完全一样。
    • provide(XHRBackend, {useClass: MockBackend}) 而不是整个 provide(Http, ...) 应该会导致相同的行为。您是否检查了来自AuthService.login() 的 Http 调用实际上是进行的?我认为您需要使 MockBackend 实际响应请求。有关示例,请参阅 angular.io/docs/ts/latest/api/http/testing/…
    • 是的,使用XHRBackend 显示相同的行为。我有来自即时使用的种子的 sn-p。关于登录调用,我正在尝试调试它,但目前我在使用 karma 时遇到了一些问题。点击调试只是打开一个新窗口,没有任何反应。 “调试失败”,就是这样……
    • 我在上面。但即使是关于 MockBackend 的文档也有问题,不能从头开始工作(grrrr),但我以某种方式解决了它并开始看到土地。如果我让它适用于我的登录示例,我将提供更多信息。我不能是唯一一个为此苦苦挣扎的人。这是一个非常基本的用例,不是吗?
    猜你喜欢
    • 1970-01-01
    • 2016-07-02
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-07-23
    相关资源
    最近更新 更多