Angular 9

如何解决Angular 9

更新 2: 事实证明,我的新测试很可能在第一次加载时响应了上一个测试的 url 参数。这显然不是我想要的,可能意味着我需要更改路由。是在 afterEach 中重置路由参数的唯一解决方案吗?有没有更好的方法来处理这个问题?

更新:

经过更多的挖掘,我不再认为这是一个异步问题,但我不确定问题是什么。我认为这与路由或订阅或其他事情有关。 以下是我向组件中提交的一些控制台日志中的失败测试场景示例。

//This is an emulated copy of what I see in the console

new request: 
> {pageId: 3,testCount: 1}
retrievePage: making the request with:
> {pageId: 3,testCount: 1}
new request:
> {id: 2,testCount: 2}
retrieveItem: making the request with:
> {id: 2,testCount: 2}
retrieveItem: made the request with: 
> {id: 2,testCount: 2}
FAILED MyComponent when the url matches item/:id should load the appropriate item
retrievePage: made the request with:
> {pageId: 3,testCount: 1}
> error properties: Object({ originalStack: 'Error: Expected spy service.retrievePage not to have been called.....

我已经更新了下面的代码片段以反映我添加的控制台日志。

正如您从日志中看到的,由于某种原因,第一次测试时,参数 subscribe 第二次运行(第一次测试运行并正常完成,记录“新请求”和“制作...”/“制作...”消息仅一次)。我不知道是否有办法确定正在运行的测试组件(如果由于某种原因生成了新组件并响应先前测试中的参数)或什么。我不确定问题是什么或如何进一步调试。

原始问题

我在测试运行一些异步代码的组件时遇到问题。由于保密协议,我无法发布实际代码,但我提供了一个与我的真实代码尽可能接近的简单表示。

我有一个像这样运行一些异步代码的组件(只包含相关位,默认导入/设置是隐含的):

...
import { ActivatedRoute } from '@angular/router';
import { Subscription } from 'rxjs';
import { MyService } from 'services/my-service';

@Component({
    ...
})
export class MyComponent implements OnInit,OnDestroy {
    private paramsSubscription: Subscription;

    constructor(private route: ActivatedRoute,private service: MyService) {}

    ngOnInit(): void {
        this.paramsSubscription = this.route.params.subscribe(async params => {
            console.log('new request: ',params);
            let result;
            let itemId = params.id;
            
            if (params.pageId) {
                
                console.log('retrievePage: making the request with: ',params);
                let page = await this.service.retrievePage(params.pageId).toPromise();
                console.log('retrievePage: made the request with: ',params);
                itemId = page.items[0].id;
            }

            if (itemId) {
                console.log('retrieveItem: making the request with: ',params);
                result = await this.service.retrieveItem(itemId).toPromise();
                console.log('retrieveItem: made the request with: ',params);
            }
            ...
        });
    }
    ngOnDestroy() {
        if (this.paramsSubscription) this.paramsSubscription.unsubscribe();
    }
}

然后我有一个这样的单元测试(也是示例代码):

...
import { fakeAsync,tick } from '@angular/core/testing';
import { of,ReplaySubject } from 'rxjs';
import { ActivatedRoute,Params } from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing';
import { MyService } from 'services/my-service';
import { MyComponent } from './my.component';

describe('MyComponent',() => {
    let component: MyComponent;
    let fixture: ComponentFixture<MyComponent>;

    let service = jasmine.createSpyObj('MyService',{
        retrieveItem: of([]),retrievePage: of([])
    });
    let route: ActivatedRoute;
    let routeParams = new ReplaySubject<Params>(1);
    
    let testCount = 0;

    let item = {
        id: 2,...
    };

    let page = {
        id: 3,items: [item]
        ...
    };

    beforeEach(async(() => {
        TestBed.configureTestingModule({
            declarations: [MyComponent],imports: [RouterTestingModule],providers: [
                { provide: MyService,useValue: service },{ provide: ActivatedRoute,useValue: 
                    jasmine.createSpyObj('ActivatedRoute',[],{
                        params: routeParams.asObservable()
                    }) 
                },]
        }).compileComponents();
    }));

    beforeEach(() => {
        fixture = TestBed.createComponent(MyComponent);
        component = fixture.componentInstance;

        service = TestBed.inject(MyService);
        route = TestBed.inject(ActivatedRoute);
        
        testCount++;

        fixture.detectChanges();
    });
    
    afterEach(() => {
        service.retrieveItem.calls.reset();
        service.retrievePage.calls.reset();
    });

    it('should create',() => {
        expect(component).toBeTruthy();
    });
    

    describe('when the url matches: item/:id',() => {
        beforeEach(fakeAsync(() => {
            routeParams.next({id: item.id,testCount: testCount});
            tick();
            fixture.detectChanges();
        }));

        it('should load the appropriate item',() => {
            expect(service.retrieveItem).toHaveBeenCalledWith(item.id);
            expect(service.retrievePage).not.toHaveBeenCalled();
        });
        ...
    });

    describe('when the url matches: item',() => {
        beforeEach(fakeAsync(() => {
            routeParams.next({testCount: testCount});
            tick();
            fixture.detectChanges();
        }));

        it('should load the first item',() => {
            expect(service.retrievePage).not.toHaveBeenCalled();
            expect(service.retrieveItem).toHaveBeenCalledWith(1);
        });
        ...
    });

    describe('when the url matches: item/:pageId',() => {
        beforeEach(fakeAsync(() => {
            routeParams.next({pageId: page.id,testCount: testCount});
            tick();
            fixture.detectChanges();
        }));

        it('should load the item from the page',() => {
            expect(service.retrievePage).toHaveBeenCalledWith(page.id);
            expect(service.retrieveItem).toHaveBeenCalledWith(page.items[0].id);
        });
        ...
    });
});

我遇到的问题是在我的单元测试中,来自我的组件的 await 调用在进入下一个单元测试之前还没有完成,所以我的期望调用无法正常运行。当我的测试以完美的顺序运行时,一切都很好。但有时我的测试以不同的顺序运行,比如 1、3、2。当他们这样做时,测试 #2 会失败,因为它期望用 id 2 调用retrieveItem,但前一个调用用 id 1 触发它。(这只是一个人为的例子,我的实际代码比这更复杂)

我想知道的是,我如何告诉 jasmine 等待在我的组件中触发的调用返回。我已经尝试查看有关 jasmine https://jasmine.github.io/tutorials/async 的异步代码的教程,但这些教程似乎都参考了在我的测试中手动进行异步调用,这不是我想要做的。

带有 tick() 的 fakeAsync 是我试图让它等待路由和一切完成的尝试,但它似乎并没有解决我的问题。有谁知道我做错了什么或如何解决我的问题?

我也试过:

  • 在刻度内输入一个值,例如滴答(10)
  • 使用fixture.whenStable().then(() => { //expect stuff})

另外,如果我添加 routeParams.next({});对我的 afterEach() 来说,它解决了这个问题,但我觉得这违背了测试随机顺序的目的,因为这意味着我的代码只有在你没有参数的情况下才能运行,在我的实际实现中你可以在这里使用url 中的任何参数(例如,我可以从 pageId: 1 到 id: 2 没有中间步骤)。

更多信息:在我的真实代码中,它是这样发生的:

//This is pseudo code
params.subscribe
    if(params.first) param1 = params.first;
    if(params.second) param2 = params.second;
    if (param1) {
        item = function1();
        param2 = item.param2;
    }
    if(param2) {
        otherItem = await function2();
    } else if (item) {
        result = await function3();
    }

我的测试是检查在激活路由传递各种参数时是否调用了函数 1、函数 2 和函数 3。我在我的组件中添加了控制台日志,以确认 test2 将启动,并且在 test2 启动后调用 test1 中的 function2。所以我的检查以确保没有调用 function2 失败,因为它从 test1 延迟。

非常感谢任何建议或提示。

解决方法

所以经过更多的挖掘和谷歌搜索之后,我设法自己找到了解决我的问题的方法,所以我在这里分享它,以防其他人和我一样困惑。

https://angular.io/guide/testing-components-scenarios#testing-with-activatedroutestub

在 angular 文档中,当他们使用 activateRoutes 时,我注意到他们首先触发路由,然后创建组件。通过观察应用程序的正常工作方式(而不是在测试期间),这更符合 angular 的实际功能 - 在创建组件之前进行路由。基于我的解决方案基于这个事实,我最终将我的测试修改为如下所示:

...
import { async } from '@angular/core/testing';
import { of,ReplaySubject } from 'rxjs';
import { ActivatedRoute,Params } from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing';
import { MyService } from 'services/my-service';
import { MyComponent } from './my.component';

describe('MyComponent',() => {
    let component: MyComponent;
    let fixture: ComponentFixture<MyComponent>;

    let service = jasmine.createSpyObj('MyService',{
        retrieveItem: of([]),retrievePage: of([])
    });
    let route: ActivatedRoute;
    let routeParams = new ReplaySubject<Params>(1);
    
    let testCount = 0;

    let item = {
        id: 2,...
    };

    let page = {
        id: 3,items: [item]
        ...
    };

    beforeEach(async(() => {
        TestBed.configureTestingModule({
            declarations: [MyComponent],imports: [RouterTestingModule],providers: [
                { provide: MyService,useValue: service },{ provide: ActivatedRoute,useValue: 
                    jasmine.createSpyObj('ActivatedRoute',[],{
                        params: routeParams.asObservable()
                    }) 
                },]
        }).compileComponents();
    }));

    beforeEach(() => {
        service = TestBed.inject(MyService);
        route = TestBed.inject(ActivatedRoute);
    });
    
    function createComponent() {
        fixture = TestBed.createComponent(MyComponent);
        component = fixture.componentInstance;

        fixture.detectChanges();
    }
    
    afterEach(() => {
        service.retrieveItem.calls.reset();
        service.retrievePage.calls.reset();
    });

    it('should create',() => {
        createComponent();
        expect(component).toBeTruthy();
    });
    

    describe('when the url matches: item/:id',() => {
        beforeEach(async () => {
            routeParams.next({id: item.id,testCount: testCount});
            createComponent();
        });

        it('should load the appropriate item',() => {
            expect(service.retrieveItem).toHaveBeenCalledWith(item.id);
            expect(service.retrievePage).not.toHaveBeenCalled();
        });
        ...
    });

    describe('when the url matches: item',() => {
        beforeEach(async () => {
            routeParams.next({testCount: testCount});
            createComponent();
        });

        it('should load the first item',() => {
            expect(service.retrievePage).not.toHaveBeenCalled();
            expect(service.retrieveItem).toHaveBeenCalledWith(1);
        });
        ...
    });

    describe('when the url matches: item/:pageId',() => {
        beforeEach(async () => {
            routeParams.next({pageId: page.id,testCount: testCount});
            createComponent();
        ));

        it('should load the item from the page',() => {
            expect(service.retrievePage).toHaveBeenCalledWith(page.id);
            expect(service.retrieveItem).toHaveBeenCalledWith(page.items[0].id);
        });
        ...
    });
});

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


Selenium Web驱动程序和Java。元素在(x,y)点处不可单击。其他元素将获得点击?
Python-如何使用点“。” 访问字典成员?
Java 字符串是不可变的。到底是什么意思?
Java中的“ final”关键字如何工作?(我仍然可以修改对象。)
“loop:”在Java代码中。这是什么,为什么要编译?
java.lang.ClassNotFoundException:sun.jdbc.odbc.JdbcOdbcDriver发生异常。为什么?
这是用Java进行XML解析的最佳库。
Java的PriorityQueue的内置迭代器不会以任何特定顺序遍历数据结构。为什么?
如何在Java中聆听按键时移动图像。
Java“Program to an interface”。这是什么意思?
Java在半透明框架/面板/组件上重新绘画。
Java“ Class.forName()”和“ Class.forName()。newInstance()”之间有什么区别?
在此环境中不提供编译器。也许是在JRE而不是JDK上运行?
Java用相同的方法在一个类中实现两个接口。哪种接口方法被覆盖?
Java 什么是Runtime.getRuntime()。totalMemory()和freeMemory()?
java.library.path中的java.lang.UnsatisfiedLinkError否*****。dll
JavaFX“位置是必需的。” 即使在同一包装中
Java 导入两个具有相同名称的类。怎么处理?
Java 是否应该在HttpServletResponse.getOutputStream()/。getWriter()上调用.close()?
Java RegEx元字符(。)和普通点?