Teste de unidade angular com Jasmine - Erro: adicione uma anotação @NgModule

Aug 22 2020

Estou tentando escrever um teste de unidade Jasmine (com Karma) para um componente Angular que usa dois serviços e um formulário. Os tutoriais de teste ( como este do Angular Docs ) mostram apenas como testar um componente com um serviço e, de alguma forma, não consigo fazer funcionar com um componente um pouco mais complexo:

Meu componente: user-login.component.ts:

O componente possui um formulário de login, onde o usuário pode inserir suas credenciais. OnSubmitEu envio as credenciais fornecidas para um serviço de autenticação, que lida com a solicitação http para minha API. Se a resposta http da API tiver status 200, ela conterá um token de login (JWT), que armazeno com outro serviço que chamei de TokenStorageService:

import { Component, OnInit } from '@angular/core';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { TokenStorageService } from '../../../_services/token-storage.service';
import { AuthenticationService } from '../../../_services/authentication.service';
import { AuthRequest } from '../../../_models/authRequest';

@Component({
  selector: 'app-user-login',
  templateUrl: './user-login.component.html',
  styleUrls: ['./user-login.component.scss']
})
export class UserLoginComponent implements OnInit {

  loginForm: FormGroup;

  constructor(private formBuilder: FormBuilder,
    private tokenStorage: TokenStorageService,
    private authService: AuthenticationService) { }

   ngOnInit() {
     this.loginForm = this.formBuilder.group({
       username: ['', Validators.compose([Validators.required])],
       password: ['', Validators.required]
     });
   }

  onSubmit() {
    this.authService.login({ 
      userName: this.loginForm.controls.username.value, 
      password: this.loginForm.controls.password.value
    })
    .subscribe(data => {  
      if (data.status === 200) {
        this.tokenStorage.saveToken(data.body)
        console.log("SUCCESS: logged in")
      } 
    }
    });
  }
}

Meu teste: user-login.component.spec.ts:

Então eu entendi que as três coisas que eu forneço no construtor ( FormBuilder, TokenStorageServicee AuthenticationService) eu também tenho que fornecer no meu TestBed. E como eu realmente não quero injetar os serviços para o teste de unidade, estou usando o Stub Services. Então eu fiz isso:

TestBed.configureTestingModule({
      imports: [{HttpClientTestingModule}],
      providers: [{provide: FormBuilder}, { provide: TokenStorageService, useValue: tokenStorageServiceStub }, { provide: AuthenticationService, useValue: authenticationServiceStub }

Todo o teste fica assim:

import { ComponentFixture, TestBed } from '@angular/core/testing';
import { UserLoginComponent } from './user-login.component';
import { FormBuilder } from '@angular/forms';
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { TokenStorageService } from 'src/app/_services/token-storage.service';
import { AuthenticationService } from 'src/app/_services/authentication.service';

describe('UserLoginComponent', () => {
  let component: UserLoginComponent;
  let fixture: ComponentFixture<UserLoginComponent>;
  let tokenStorageServiceStub: Partial<TokenStorageService>;
  let authenticationServiceStub: Partial<AuthenticationService>;
  // let tokenStorageService;
  // let authenticationService;

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [{HttpClientTestingModule}],
      providers: [{provide: FormBuilder}, { provide: TokenStorageService, useValue: tokenStorageServiceStub }, { provide: AuthenticationService, useValue: authenticationServiceStub } ],
      declarations: [ UserLoginComponent ]
    })
    fixture = TestBed.createComponent(UserLoginComponent);
    component = fixture.componentInstance;
    // tokenStorageService = TestBed.inject(TokenStorageService);
    // authenticationService = TestBed.inject(AuthenticationService);
    fixture.detectChanges();
  });

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

Comentei 4 linhas porque acho que estão errados, mas no exemplo do Angular Docs eles também estão injetando os serviços reais, mesmo que digam que não querem usar os serviços reais no teste. Não entendo essa parte do exemplo do Docs.

Mas de qualquer forma, continuo recebendo esta mensagem de erro:

Já que o erro diz algo sobre @NgModuleeu acho que pode ter a ver com meu app.module.tsarquivo? Aqui está o meu app.module.ts:

@NgModule({
 declarations: [
   AppComponent,
   SidebarComponent,
   UsersComponent,
   DetailsComponent,
   ProductsComponent,
   UploadFileComponent,
   GoogleMapsComponent,
   AddUserComponent,
   ProductFormComponent,
   UserLoginComponent,
   EditUserComponent,
   ProductDetailsComponent,
   MessagesComponent,
   MessageDetailsComponent,
   ChatComponent,
   UploadMultipleFilesComponent,
   InfoWindowProductOverviewComponent,
   AddDormComponent,
   AddProductComponent
 ],
 imports: [
   BrowserModule,
   AppRoutingModule,
   HttpClientModule, 
   BrowserAnimationsModule,
   FormsModule,
   ReactiveFormsModule,
   ImageCropperModule,
   DeferLoadModule,
   //Angular Material inputs (spezielle UI Elemente)
   MatDatepickerModule,
   MatInputModule,
   MatNativeDateModule,
   MatSliderModule,
   MatSnackBarModule,
   MatSelectModule,
   MatCardModule,
   MatTooltipModule,
   MatChipsModule,
   MatIconModule,
   MatExpansionModule,
   MDBBootstrapModule,
   AgmCoreModule.forRoot({
     apiKey: gmaps_environment.GMAPS_API_KEY 
   })
  ],
  providers: [
   UploadFileService, 
   {provide: MAT_DATE_LOCALE, useValue: 'de-DE'},   
   {provide:HTTP_INTERCEPTORS, useClass:BasicAuthHttpInterceptorService, multi:true},
 ],   
 bootstrap: [AppComponent],
})
export class AppModule { }

Respostas

5 tilo Aug 25 2020 at 19:20

No momento, você declarou apenas os stubs tokenStorageServiceStube authenticationServiceStub, mas precisa inicializá-los antes de fornecê-los. Algo nesse sentido:

tokenStorageServiceStub = {
  saveToken: () => {}
};


authenticationServiceStub = {
  login: () => of({status: 200, body: {}})
}

Além disso, leve em consideração a recomendação de @PrincelsNinja.

4 Prince Aug 25 2020 at 19:08

Você pode remover o FormBuilderda providersmatriz em seus casos de teste e importar em seu ReactiveFormsModulelugar.

TestBed.configureTestingModule({
      imports: [HttpClientTestingModule, ReactiveFormsModule],
      providers: [{ provide: TokenStorageService, useValue: tokenStorageServiceStub }, { provide: AuthenticationService, useValue: authenticationServiceStub }

Nota: Não coloque os elementos de importação entre chaves.

1 sehe Aug 25 2020 at 19:25

Seu app.module.tsarquivo é irrelevante ao usar a bancada de teste. Verifique todas as importações de seus componentes, coloque-os em sua .configureTestBedimportação e seja especialmente cuidadoso com as diretivas.

Muitas vezes recebo esse erro quando uma importação está faltando.

1 SouritraDasGupta Sep 01 2020 at 00:21
  providers: [
              ReactiveFormsModule,
              { provide: TokenStorageService,
                useClass: {saveToken: (data: any) => data} },
              { provide: AuthenticationService,
                useClass: {
                           login: (loginDetails: any): Promise<any> => {
                                                   return {status: 200}}.toPromise(); } }
                          }
               },
             ]
                     

Além disso, remova as importações para TokenStorageService e AuthenticationService de seus arquivos de especificação para ver se são eles que estão causando o primeiro erro indefinido no módulo de teste dinâmico.

uiTeam324 Aug 22 2020 at 14:55

Pode ser porque ele não está obtendo os arquivos do caminho que você mencionou. Use o mesmo caminho mencionado no arquivo do componente e tente. Na maioria das vezes, esse tipo de problema ocorre devido ao caminho do arquivo errado, declarações duplicadas, não declarações etc.

import { TokenStorageService } from '../../../_services/token-storage.service';
import { AuthenticationService } from '../../../_services/authentication.service';