【问题标题】:Angular Material components fail testsAngular Material 组件未通过测试
【发布时间】:2018-10-12 07:02:55
【问题描述】:

我有一个用 Angular 制作的 Sidenav 菜单。组件如下所示:

模板:

 <mat-toolbar color="primary" [fxLayoutAlign]="(settings.menuType != 'mini') ? 'space-between center' : 'center center'" class="sidenav-header">
    <a mat-raised-button color="accent" routerLink="/" (click)="closeSubMenus()" class="small-logo">Menu</a>
    <a *ngIf="settings.menuType == 'default'" class="logo" routerLink="/" (click)="closeSubMenus()">mEMS</a> 
</mat-toolbar>

<div fxLayout="column" fxLayoutAlign="center center" class="user-block transition-2" [class.show]="settings.sidenavUserBlock"> 
    <div [fxLayout]="(settings.menuType != 'default') ? 'column' : 'row'" 
         [fxLayoutAlign]="(settings.menuType != 'default') ? 'center center' : 'space-around center'" class="user-info-wrapper">
        <div class="user-info">
            <p class="name"> {{currentUser}}</p>
            <p *ngIf="settings.menuType == 'default'" class="position">Rocket Scientist<br> </p>
        </div>
    </div>
    <div *ngIf="settings.menuType != 'mini'" fxLayout="row" fxLayoutAlign="space-around center" class="w-100 muted-text">
       <a mat-icon-button (click)="logout();" routerLink="/login">
            <mat-icon>power_settings_new</mat-icon>
        </a>
    </div>
</div>

<div id="sidenav-menu-outer" class="sidenav-menu-outer" perfectScrollbar [class.user-block-show]="settings.sidenavUserBlock">    
    <span *ngIf="!menuItems">loading....</span>
    <app-vertical-menu [menuItems]="menuItems" [menuParentId]="0"></app-vertical-menu> 
</div>

实际组件:

import { Component, OnInit, ViewEncapsulation } from '@angular/core';
import { AppSettings } from '../../../app.settings';
import { Settings } from '../../../app.settings.model';
import { MenuService } from '../menu/menu.service';
import { AuthService } from '../../../auth.service';
import { Router, ActivatedRoute } from '@angular/router';

@Component({
  selector: 'app-sidenav',
  templateUrl: './sidenav.component.html',
  styleUrls: ['./sidenav.component.scss'],
  encapsulation: ViewEncapsulation.None,
  providers: [ MenuService ]
})
export class SidenavComponent implements OnInit {

  currentUser: String;
  public userImage= '../assets/img/users/user.jpg';
  public menuItems:Array<any>;
  public settings: Settings;
  constructor(public authService: AuthService, public appSettings:AppSettings, public menuService:MenuService,public router:Router,){
      this.settings = this.appSettings.settings; 
  }

  logout() {
    this.authService.logout();
  }

  ngOnInit() {
    let jwt = localStorage.getItem(AuthService.USER_TOKEN_KEY);
    let jwtData = jwt.split('.')[1]
    let decodedJwtJsonData = window.atob(jwtData)
    let decodedJwtData = JSON.parse(decodedJwtJsonData)
    console.log(decodedJwtData);
    this.currentUser = decodedJwtData.sub;

    this.menuItems = this.menuService.getVerticalMenuItems();
  }
}

我的测试是默认的基本测试:

import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { SidenavComponent } from './sidenav.component';
import { MatButtonModule, MatFormFieldModule, MatInputModule, MatRippleModule, MatCardModule, MatSidenavModule, MatToolbarModule, MatIconModule } from '@angular/material';

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

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [ SidenavComponent ]
    })
    .compileComponents();
  }));

  beforeEach(() => {
    fixture = TestBed.createComponent(SidenavComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

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

每次我运行这个测试时,我都会因为材料组件而收到这些错误

Can't bind to 'fxLayoutAlign' since it isn't a known property of 'mat-toolbar'.
    1. If 'mat-toolbar' is an Angular component and it has 'fxLayoutAlign' input, then verify that it is part of this module.
    2. If 'mat-toolbar' is a Web Component then add 'CUSTOM_ELEMENTS_SCHEMA' to the '@NgModule.schemas' of this component to suppress this message.



    1. If 'app-vertical-menu' is an Angular component, then verify that it is part of this module.
    2. If 'app-vertical-menu' is a Web Component then add 'CUSTOM_ELEMENTS_SCHEMA' to the '@NgModule.schemas' of this component to suppress this message. ("-block-show]="settings.sidenavUserBlock">
        <span *ngIf="!menuItems">loading....</span>

我已经添加到 app.module.ts 中的所有上述组件

import { MatButtonModule, MatFormFieldModule, MatInputModule, MatRippleModule, MatCardModule, MatSidenavModule, MatToolbarModule, MatIconModule } from '@angular/material';

并导出它们......所以我有点迷茫,如果我应该以不同的方式导入/导出这些模块,我不明白我做错了什么。

【问题讨论】:

  • 当然,您需要在 mat-toolbar 中使用 fxLayoutAlign 而不是 [fxLayoutAlign]。你的app-vertical-menu 组件是在哪里定义的?
  • 你的意思是它在测试中的定义?我可能错过了,但无论如何它不应该返回重大错误。
  • 问题是你试图使用一个选择器,你没有从哪个组件导入,所以它问你“这个组件在哪里”
  • 我在测试中添加了 VerticalMenuComponent: beforeEach(async(() => { TestBed.configureTestingModule({ declarations: [ SidenavComponent, VerticalMenuComponent ] }) ,但 mat-toolbar 错误不断出现。
  • 在您发布的代码中,您只有declarations: [ SidenavComponent ]。您添加了&lt;app-vertical-menu [menuItems]="menuItems" [menuParentId]="0"&gt;&lt;/app-vertical-menu&gt;,但是这个app-vertical-menu 选择器来自哪里?就像&lt;my-app&gt;&lt;/my-app&gt; 这样的选择器一样,您需要一个源组件。

标签: angular angular-material angular5 angular-test


【解决方案1】:

错误很明显,您的组件不理解 flexLayout 指令,因为您没有在测试模块中导入它。

import { FlexLayoutModule } from '@angular/flex-layout';

你缺少的是对测试模块的理解。基本上,它用于重新创建组件的上下文,以便能够搁置 Angular 在后台使用的所有依赖项。 这就是为什么您需要重新导入您在声明组件的原始模块中使用的模块。

例如,这是我的测试模块之一:

    TestBed.configureTestingModule({
        imports: [
          CommonModule,
          BrowserAnimationsModule,
          ReactiveFormsModule,
          MaterialModule,
          FlexLayoutModule,
          RouterTestingModule,
        ],
        declarations: [
          SomeComponent
        ],
        providers: [
          // Some examples of stubs used
          { provide: SomeService, useClass: SomeServiceStub },
          { provide: MatDialogRef, useValue: {} },
          { provide: ActivatedRoute, useValue: { 'params': Observable.from([{ 'id': 1 }]) } }
        ],
        schemas: [
          NO_ERRORS_SCHEMA
        ]
      });

您的第二个错误是因为您的组件可能使用了app-vertical-menu 组件,并且由于您不想在单元测试中使用它,因此您应该在测试模块中使用NO_ERRORS_SCHEMA 声明。

如果您想编写更复杂的测试,例如集成测试,您可以使用另一个测试模块定义另一个测试套件,这将启动两个组件(您必须声明两者)。

这是一个例子:

  describe('Integration Test of SomeComponent', () => {
    // fixtures and stuff declaration

    beforeEach(() => {
      TestBed.configureTestingModule({
        imports: [
          CommonModule,
          BrowserAnimationsModule,
          ReactiveFormsModule,
          MaterialModule,
          FlexLayoutModule,
          RouterTestingModule,
        ],
        declarations: [
          SomeComponent,
          OtherComponent // Here the other one is declared
        ],
        providers: [
          { provide: SomeService, useClass: SomeServiceStub }, {
            provide: MatDialogRef, useValue: {}
          }
        ]
        // No more NO_ERROR_SCHEMA here
      });

      fixture = TestBed.createComponent(SomeComponent);
      componentInstance = fixture.componentInstance;
      fixture.detectChanges();
    });

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

希望这会有所帮助。


编辑:这是我使用 ServiceStub 而不是服务的组件的示例。我没有实际调用 getTheme() 并执行 HTTP 调用,而是使用覆盖此方法并返回静态数据的存根。这可以避免使用 Angular HTTP 依赖项的完整服务,该依赖项可能还具有其他内部 Angular 依赖项等。

// Outside of your test class
export class SomeServiceStub {
  constructor() {}

  getTheme(): Observable<Theme[]> {
    const themes: Theme[] = [
      {
        id: 128,
        code: 'EEE',
        libelle: 'Fake label 1'
      }, {
        id: 119,
        code: 'DDD',
        libelle: 'Fake label 2'
      }
    ];
    return Observable.of(themes);
  }

  // Other services methods to override...
}

// In your testing module :
providers: [
  { provide: SomeService, useClass: SomeServiceStub }, 
  { provide: MatDialogRef, useValue: {} }
]

【讨论】:

  • 确实如此,很多。我现在明白这些组件在测试时需要所有依赖项。我唯一的问题和误解......或者更好但缺乏知识是如何独立测试组件。我不想测试基于其他组件并在其构造函数中有服务的组件等等(集成测试),因为据我所知,如果其中一个服务关闭,组件测试将失败。我只想要一个简单的组件测试,没有任何路由等。这是我第一次使用 Angular 进行牛仔竞技表演,我需要弄清楚很多事情。
  • 这就是你使用存根来测试你的组件的原因。您需要做的就是配置一个动态模块,该模块将使用依赖项和类似的东西重新创建您的组件上下文,但您可以使用模拟类和存根覆盖它们。这就是为什么在单元测试(第一个示例)中,您可以看到我使用 useClass : SomeServiceStub 覆盖了我的原始服务。目的是覆盖该服务中最初会进行 HTTP 调用的方法,并使它们返回一个立即完成的 Observable。这样,你的“依赖”实际上就是一个解析静态数据的类。
  • 您可以使用RouterTestingModule from this import 对路由器执行相同操作:import { RouterTestingModule } from '@angular/router/testing';,以便为您的测试加载的唯一真实的东西就是您的组件。
  • 我确实补充说,因为我有另一个正在通过的测试正在使用它,问题是在导入所有内容之后 beforeEach(async(() => { TestBed.configureTestingModule({ declarations: [ SidenavComponent ,VerticalMenuComponent],导入:[HttpClientTestingModule, MatToolbarModule, MatIconModule, FlexLayoutModule, RouterTestingModule],提供者:[AppSettings, AuthService],模式:[ NO_ERRORS_SCHEMA ] }).compileComponents(); }));我只返回一个抛出的 [object ErrorEvent],没有别的,所以我被困在那里:)
  • 这可能是由于您的提供者,您仍在使用真实的服务,并且这种依赖链可能会导致一些难以检测的错误。我正在编辑我的答案以提供一个存根示例。
猜你喜欢
  • 2018-08-01
  • 1970-01-01
  • 2019-02-23
  • 2018-05-14
  • 2018-01-01
  • 2018-11-27
  • 2018-03-24
  • 1970-01-01
  • 2022-12-03
相关资源
最近更新 更多