广告 ·ultimatecourses.com
用终极课程正确地学习Angular”></一个></div>
          <article class=

NgRx测试:组件

学习如何使用NgRx和Jest对Angular组件进行单元测试。

下载

在这篇文章中,我将引用一个使用NgRx的非常基础的演示应用中的代码。您可以克隆存储库或下载源代码的zip文件:

系列

本文是使用Jest测试NgRx系列文章的一部分:

堆栈

此应用程序的堆栈将是:

  • 角6<李>NgRx 6<李>Jest和angular的Jest预置模块<李>jasmine-marbles模块<李>伪造伪造数据

Jest测试运行器

我们将使用Jest为一个使用NgRx进行状态管理的Angular应用编写单元测试。它比Karma快得多(即使使用无头铬),并且使用类似jasmine的API。

如果你是笑话新手,<一个href=”//www.nxtmastery.com/2018/05/26/angular-jest-testing/">看看我关于在Angular中使用Jest的文章

jasmine-marbles

我们将使用jasmine-marbles模块,以模拟可观察对象。

这里有一些链接到更多关于用NgRx测试大理石的信息:

总之,我们可以使用类似大理石图的字符串来描述一个随时间变化的可观察流。这使我们能够同步地测试异步可观察流。

我们将使用两个主要函数:

  • 热()创建一个热的可观察流。<李>冷()创建一个冷的可观察流。

我们将使用这些函数断言应用程序中特定操作的结果将产生我们所描述的预期的热或冷可观察流。

大理石测试使用一个字符串来描述可观察流:

  • -一个破折号表示一段虚拟的时间,也就是10毫秒,但这并不是很重要。<李>a-z0-9任何字母数字字符都表示一个值。我们可以使用第二个参数热()冷()函数来指定每个标记所代表的值。<李>()对数将值组合到一个单独的帧中。<李>^胡萝卜表示一个冰冷的可观察流中订阅的开始。<李>|管道表示完成通知。<李>#井号(或哈希符号)表示错误通知。

容器和演讲

在示例应用程序中,我使用了将“容器”组件与“表示”组件分离开来的组件体系结构。总而言之:

  • 容器组件检索数据并更改应用程序的状态。<李>表示组件是高度可重用的组件,不依赖于应用程序的状态。它们依赖于输入到组件并将事件作为输出发出的数据。

索引组件

让我们来看看如何在一个使用NgRx的应用中测试一个容器组件。

首先,让我们在src / app /用户/集装箱/ index.component.spec.ts文件:

描述(“IndexComponent”,()= >{组件:IndexComponent;夹具:ComponentFixture<IndexComponent>;退订=主题<无效>();beforeEach(异步(()= >{试验台configureTestingModule({声明:[IndexComponent,UserListComponent],进口:[RouterTestingModule],供应商:[{提供:商店,useValue:{调度:开玩笑fn(),:开玩笑fn()}}]})compileComponents();}));beforeEach(()= >{夹具=试验台createComponent(IndexComponent);组件=夹具componentInstance;});afterEach(()= >{退订下一个();退订完整的();});});

让我们回顾一下测试的设置:

  • 如您所见,Jest使用类似jasmine的方法来设置测试套件,使用描述(),以及全局方法,如beforeEach ()afterEach ()<李>首先,我们设置试验台在第一个beforeEach ()方法。<李>我们添加子组件IndexComponentUserListComponent宣言数组,就像我们在模块中添加的那样。<李>我们正在进口RouterTestingModule嘲笑的< router-outlet >元素。<李>我们正在模拟一个对象商店,具有两个性质:调度()管()<李>然后,在第二个beforeEach ()方法创建组件夹具并存储对组件的引用实例<李>最后,我们使用afterEach ()方法取消测试中的任何订阅takeUntil在RxJS运营商。

标准的“it should create”测试如下:

(“应该创建”,()= >{预计(组件)toBeTruthy();});

没什么特别的。当我们使用Angular CLI生成组件时,这是直接开箱即用的。

让我们看看测试ngOnInit ()方法:

ngOnInit(){商店调度(LoadUsers());用户=商店(选择(selectAllUsers));}

我们只是在发送LoadUsers操作,并使用selectAllUsers选择器来获取存储中的用户数组,它恰好是@ngrx/entity的集合用户对象。

让我们测试它:

描述(“ngOnInit()”,()= >{('应该在ngOnInit生命周期中调度LoadUsers动作',()= >{常量行动=LoadUsers();常量商店=试验台得到(商店);常量间谍=开玩笑spyOn(商店,“调度”);夹具detectChanges();预计(间谍)toHaveBeenCalledWith(行动);});(“应该selectAllUsers”,()= >{常量商店=试验台得到(商店);常量用户=generateUsers();商店=开玩笑fn(()= >(“——”,{一个:用户}));夹具detectChanges();常量预期=(“——”,{一个:用户});预计(组件用户)toBeObservable(预期);});});

我们想测试这两个调度()管()控件中调用的方法ngOnInit ()方法。

首先,我们创建一个测试来分派LoadUsers行动:

  • 首先,我们更新LoadUsersaction类。<李>然后,我们得到对商店使用TestBed.get ()方法。<李>然后我们安排了一个间谍调度()方法使用jest.spyOn ()函数。<李>然后我们调用ngOnInit ()方法触发变更检测detectChanges ()方法对夹具。<李>最后,我们期待调度()方法来调用LoadUsers行动。

然后,我们创建一个从state对象中选择用户的测试:

  • 首先,我们得到一个引用商店使用TestBed.get ()方法。<李>接下来,我使用导出generateUsers ()函数生成一个假数组用户对象。我正在使用faker库来生成一些假数据。<李>接下来,我们要嘲笑管()方法商店实例。模拟函数返回一个新的热()可观察到的流。可观察流由一个帧(虚拟“时间”已经通过)和一个值组成,一个,也就是用户数组中。

最后,我们应该编写一个单元测试来验证用户属性是一个数组的冷观察对象用户对象:

描述(“用户”,()= >{('应该是用户对象数组的可观察对象',完成= >{常量用户=generateUsers();常量商店=试验台得到(商店);商店=开玩笑fn(()= >(“——|”,{一个:用户}));夹具detectChanges();组件用户订阅(componentUsers= >{预计(componentUsers)toEqual(用户);完成();});getTestScheduler()冲洗();});});

在这个测试:

  • 首先,我们生成一个假数组用户对象的使用generateUsers ()函数。<李>我们得到一个引用商店对象注入到我们的组件中TestBed.get ()方法。<李>我们要模拟方法返回的冷可观测流用户对象。我们正在创造一个冷的可见物体,因为HttpClient返回一个冷观测值。<李>然后我们调用ngOnInit ()方法触发变更检测detectChanges ()方法对夹具。<李>后调用ngOnInit ()在我们的方法组件,我们可以订阅()用户性质,期望最后一个()notification是用户数组。<李>注意,我们使用(完成)Jest提供的信号异步测试完成的函数。<李>最后,我们需要冲洗()测试调度程序中的任务。这将触发cold observable发出通知。

用户列表组件

现在我们已经测试了我们的容器IndexComponent类,让我们看一个测试示例UserListComponent。元素的子元素IndexComponent并被设计成无状态的表示组件。

让我们来看看组件模板src / app /用户/组件/用户列表/ user-list.component.html:

<ul><* ngFor=让用户的用户trackBy=user.id><一个href=javascript:无效(0)(点击)=selectUser.emit(用户)>{{用户。firstName}} {{user.lastName}}一个>>ul>

如您所见,我们只是呈现了一个无序的用户列表。这里没有什么特别的。

首先,我们想测试用户列表是否呈现在其中src / app /用户/组件/用户列表/ user-list.component.spec.ts:

(应该显示一个无序的英雄列表,()= >{常量ulDebugEl=夹具debugElement查询(通过css(“ul”));常量ulEl=ulDebugElnativeElement作为HTMLUListElement;组件用户=用户;夹具detectChanges();预计(ulElchildElementCount)托比(用户长度);常量firstLi=ulElquerySelector(“李:第一个孩子”);预计(firstLitextContent)toEqual($ {用户[0]firstName}$ {用户[0]});});

这个测试非常直接:

  • 首先,我们得到引用DebugElement对于无序列表元素。<李>然后,我们得到对HTMLUListElement对象,即nativeElement财产的DebugElement。注意,我们必须强制转换类型,以显式地告诉TypeScript DOM元素是具有类型的HTMLUListElement<李>然后我们设置用户属性。在这个例子中,我有一个数组用户对象,该对象存储在名为用户调用的结果generateUsers ()函数,该函数使用faker生成假数据。<李>接下来,我们在Angular中使用detectChanges ()方法对夹具。<李>我们期望()这一childElementCount的无序列表的长度应该与用户数组的长度匹配。<李>我们进一步获得第一个列表项元素,并期望textContent等于呈现的第一个用户的姓和名。

现在,我们断言EventEmitter触发被点击的用户:

(“点击时应选择用户”,()= >{常量用户=用户[0];组件用户=用户;夹具detectChanges();常量anchorDebugEl=夹具debugElement查询(通过css('ul > li:第一个孩子> a'));selectedUser:用户;组件selectUser订阅(用户= >(selectedUser=用户));anchorDebugEltriggerEventHandler(“点击”,用户);预计(selectedUser)toEqual(用户);});

让我们回顾一下测试:

  • 首先,存储对第一个对象的引用用户(我们将断言它是被点击/触摸的用户)。<李>和之前的测试一样,我们设置了用户的数组用户我们使用faker生成的对象。<李>然后,我们在Angular中触发变更检测。<李>使用DebugElement我们得到一个引用<一>元素,该元素是列表中第一个列表项的直接后代。<李>我们订阅EventEmitterselectedUser输出事件。<李>使用triggerEventHandler ()方法,我们可以触发点击事件,提供用户事件对象。<李>最后,我们期望()这一selectedUser等于用户在单击事件中触发的对象。

运行测试

现在是时候停下来运行我们的测试,以确保一切都通过了:

美元npm测试

添加用户组件

好了,现在我们有测试IndexComponent它的孩子UserListComponent,让我们看看测试AddComponent

这里是模板src / app /用户/集装箱/添加/ add.component.html:

<app-user-form(userChange)=onUserChange(事件)>app-user-form>

我们将使用UserFormComponent若要显示用于添加新用户的表单,请附加@Output ()绑定的userChange,调用onUserChange ()方法,当触发值时。

AddComponent我们定义的onUserChange ()方法src / app /用户/集装箱/添加/ add.component.ts:

出口AddComponent{构造函数(私人商店:商店<fromRoot状态>){}onUserChange(用户:用户){商店调度(AddUser({用户:用户}));}}

onUserChange ()方法我们调度()AddUser行动到商店。

好的,我们来测试一下AddUser动作被分派时onUserChange发出的src / app /用户/集装箱/添加/ add.component.spec.ts:

(当onUserChange被调用时应该调度AddUser动作,()= >{常量用户=generateUser();常量行动=AddUser({用户});常量间谍=开玩笑spyOn(商店,“调度”);夹具detectChanges();组件onUserChange(用户);预计(间谍)toHaveBeenCalledWith(行动);});
  • 首先,我们用generateUser ()函数,该函数从模型中导入以创建假的用户对象使用伪造者。<李>我们重新开始AddUser行动,指定有效载荷这是用户对象。<李>然后,制造一个间谍调度()方法在存储中使用jest.spyOn ()<李>然后触发变更检测并调用onUserChange ()方法与用户对象。<李>最后,我们断言调度()方法调用AddUser行动。

编辑用户组件

除了能够添加用户之外,我们还希望能够编辑现有用户。的EditComponent模板非常类似于AddComponent,加上用户输入绑定:

<app-user-form(用户)=$ |用户异步(userChange)=onUserChange(事件)>app-user-form>

EditComponentid参数,以指定要编辑的用户。我们将使用id参数来分派两个动作:SelectUserLoadUser

这是EditComponent源在src / app /用户/集装箱/编辑/ edit.component.ts:

出口EditComponent实现了OnInit{用户美元:可观测的<用户>;构造函数(私人activatedRoute:ActivatedRoute,私人商店:商店<fromRoot状态>){}ngOnInit(){常量PARAM_ID=“id”;用户美元=activatedRouteparamMap(过滤器(paramMap= >paramMap(PARAM_ID)),地图(paramMap= >paramMap得到(PARAM_ID)),利用(id= >{商店调度(SelectUser({id:+id}));商店调度(LoadUser({id:+id}));}),switchMap(id= >商店(选择(selectSelectedUser))));}onUserChange(用户:用户){商店调度(UpdateUser({用户:用户}));}}

在开始测试组件之前,我们需要连接试验台:

beforeEach(异步(()= >{试验台configureTestingModule({声明:[EditComponent,UserFormComponent],进口:[FormsModule,RouterTestingModule,ReactiveFormsModule],供应商:[{提供:ActivatedRoute,useValue:{paramMap:BehaviorSubject(convertToParamMap({id:用户id}))}},{提供:商店,useValue:{调度:开玩笑fn(),:开玩笑fn(()= >(“——”,{一个:用户}))}}]})compileComponents();}));

我们来回顾一下beforeEach ()方法:

  • 首先,我们指定EditComponentUserFormComponent就像我们在模块里做的那样。<李>我们添加必要的进口用于处理响应式表单和路由器。<李>在供应商数组,我们需要模拟ActivatedRoute商店。我们使用一个BehaviorSubject嘲笑的paramMap财产的ActivatedRoute,指定id假用户的。我们模仿了这两个调度()管()方法商店。请注意,管()mock返回一个hot observable,它会在单个帧之后发出user对象。

与我们的试验台准备好了,我们来测试一下ngOnInit ()生命周期方法:

描述(“ngOnInit”,()= >{('should dispatch SelectUser action for specified id parameter',()= >{常量行动=SelectUser({id:用户id});常量间谍=开玩笑spyOn(商店,“调度”);夹具detectChanges();预计(间谍)toHaveBeenCalledWith(行动);});(应该为指定的id参数调度LoadUser动作,()= >{常量行动=LoadUser({id:用户id});常量间谍=开玩笑spyOn(商店,“调度”);夹具detectChanges();预计(间谍)toHaveBeenCalledWith(行动);});});

我们的“ngOnInit”测试套件包含两个测试SelectUserLoadUser行动。

接下来,让我们测试onUserChange ()方法时调用的userChangeEventEmitter对象发出更新后的user对象<用户表单>组件。

描述(“onUserChange”,()= >{(当onUserChange被调用时应该分派UpdateUser动作,()= >{常量用户=generateUser();常量行动=UpdateUser({用户});常量间谍=开玩笑spyOn(商店,“调度”);夹具detectChanges();组件onUserChange(用户);预计(间谍)toHaveBeenCalledWith(行动);});});

这与AddComponent

用户表单组件

AddComponentEditComponent测试后,下一个逻辑步骤是为UserFormComponent

这里是模板src / app /用户/组件/用户表单/ user-form.component.html:

<形式(formGroup)=形式><输入类型=文本占位符=第一个名字formControlName=firstName/><输入类型=文本占位符=formControlName=/><按钮(点击)=onSave ()>保存按钮>形式>

我用的是ReactiveFormsModule为用户创建一个简单的表单,其中包含两个输入:姓和名。当点击“保存”按钮时,onSave ()方法将被调用。

UserFormComponent我们定义的onSave ()方法:

onSave(){如果(形式无效的){返回;}userChange发出({用户,形式价值});}

首先,让我们断言表单值在ngOnChanges ()调用生命周期方法:

(“应该将值修补到表单中”,()= >{常量用户=generateUser();组件ngOnChanges({用户:SimpleChange(,用户,真正的)});预计(组件形式价值)toEqual({firstName:用户firstName,:用户});});

在这个测试中,我们:

  • 生成一个假用户<李>调用ngOnChanges ()方法,提供一个新的SimpleChange对象的一个用户<李>预计,价值FormGroup包括用户的名和姓。

然后,我们断言userChange当表单被提交时,输出事件被触发:

('提交时应该触发userChange事件',()= >{常量用户=generateUser();常量firstName=“布莱恩”;常量firstNameDebugEl=夹具debugElement查询(通过css(“输入(formControlName =“firstName”)”));常量firstNameEl=firstNameDebugElnativeElement作为HTMLInputElement;常量buttonDebugEl=夹具debugElement查询(通过css(“按钮”));夹具detectChanges();updatedUser:用户;组件userChange订阅(用户= >(updatedUser=用户));组件用户=用户;组件ngOnChanges({用户:SimpleChange(,用户,真正的)});firstNameEl价值=firstName;firstNameEldispatchEvent(newEvent(“输入”));buttonDebugEltriggerEventHandler(“点击”,);预计(updatedUser)toEqual({用户,firstName});});

为了测试我们的组件,我们需要:

  • 提供一个假用户作为组件的输入。<李>触发ngOnChanges ()方法来填充表单patchValue ()方法FormGroup<李>订阅userChange输出。<李>的值更新firstName输入。<李>触发点击事件。

在用初始用户数据填充表单之后,更新第一个名称输入值,并订阅userChangeEventEmitter我们可以期望()的更新用户发出的值等于提供的用户,带有update first name值。

结论

单元测试非常有价值,使用像Jest这样的测试运行器可以使快速迭代的体验变得愉快。

非常感谢NgRx社区,以及所有关于在Angular中使用Rx弹珠对observable和异步事件进行单元测试的文档。

布莱恩F爱

嗨,我是布莱恩。我对TypeScript、Angular和Node.js感兴趣。我和我最好的朋友邦妮结婚了,我住在波特兰,我经常滑雪。