Angular Unit Testing with Jasmine - Error: Please add an @NgModule annotation

Viewed 1795

I'm trying to write a Jasmine Unit Test (with Karma) for an Angular component that uses two services and one form. The tutorials on testing (like this one from the Angular Docs) show only how to test a component with one service and somehow I can't make it work with a bit more complex component:

My component: user-login.component.ts:

The component has a login form, where the user can put in his credentials. OnSubmit I send the provided credentials to an Authentication Service, which handles the http request to my API. If the http response from the API has status 200 it will contain a login token (JWT), which I store with another service that I called 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")
      } 
    }
    });
  }
}

My test: user-login.component.spec.ts:

So I understood that the three things I provide in the constructor (FormBuilder, TokenStorageService and AuthenticationService) I also have to provide in my TestBed. And since I don't really want to inject the services for the Unit test I'm using Stub Services instead. So I did this:

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

The whole test then looks like this:

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();
  });
});

I commented 4 lines out since I think they are wrong, but in the the Angular Docs example they are also injecting the real services, even tough they say they don't want to use the real services in the test. I don't understand that part in the Docs example?

But either way I keep getting this Error message:

enter image description here

Since the error says something about @NgModule I think it may has to do with my app.module.ts file? Here is my 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 { }
5 Answers

Right now you only declared the stubs tokenStorageServiceStub and authenticationServiceStub, but you need to initialize them before providing them. Something along those lines:

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


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

Additionally, take the recommendation by @PrincelsNinja into account.

Can you please remove the FormBuilder from the providers array in your test cases and import ReactiveFormsModule instead.

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

Note: Don't enclose the imports elements inside curly braces.

It might be because of it's not getting the files from the path you mentioned. Use the same path as mentioned on the component file and try. Mostly this kind issue comes of because of wrong file path, duplicate declarations, not declarations etc.

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

Your app.module.ts file is irrelevant when using the test bed. Please check all imports of your components, put them into your .configureTestBed-import and be especially careful with directives.

I quite often get this error when an import is missing.

  providers: [
              ReactiveFormsModule,
              { provide: TokenStorageService,
                useClass: {saveToken: (data: any) => data} },
              { provide: AuthenticationService,
                useClass: {
                           login: (loginDetails: any): Promise<any> => {
                                                   return {status: 200}}.toPromise(); } }
                          }
               },
             ]
                     

Also, remove the imports for TokenStorageService and AuthenticationService from your spec files to see if those are the ones causing the first undefined error in dynamictestingmodule.

Related