Strah Behry
Strah Behry

Reputation: 581

Angular Unit tests failing with undefined / no provider

I could really require some help I have about 12 component that I am failing to test because of the same issues. I've been sitting on this for about 15 hours and really haven't made much progress I think maybe my mocks are wrong. I'll just pick a random one as an example of the problems.

I am using Karma and Jasmine to test within an Angular 10 development environment

The component is called ArchivedUserStoryOverview and I've made an own controller to interact with Firebase, I am mocking out the observables this would return completely (or at least that's what I think I am doing). It's important to note that my app runs without bugs issues that occur when I test.

Actual Component: Archiveduserstoryoverview.component.ts

import { Component, OnInit } from '@angular/core';
import { FirebaseController } from '../../services/firebase-controller';
import { ActivatedRoute } from "@angular/router";

@Component({
  selector: 'app-archiveduserstoryoverview',
  templateUrl: './archiveduserstoryoverview.component.html',
  styleUrls: ['./archiveduserstoryoverview.component.css']
})
export class ArchiveduserstoryoverviewComponent implements OnInit {
  projectId: string;
  userstoryArray: Array<any>;
  assigneeArray: Array<string>;

  constructor(public firebaseController: FirebaseController, private route: ActivatedRoute) { 
    this.userstoryArray = new Array<any>();
    this.assigneeArray = new Array<string>();
    this.route.params.subscribe(params => this.setProjectId(params["id"]));
  }

  ngOnInit(): void {
    this.firebaseController.getUserstoriesSnapshot().subscribe(res => {
      res.forEach(item => {
        if(item.payload.val()['ProjectId'] == this.projectId){
          this.userstoryArray.push([item.key, item.payload.val()]);
          this.getUserNameByKey(item.payload.val()['AssignedUser']);
        }
      })
    });
  }

  setProjectId(id){
    this.projectId = id;
  }

  // Returns the username for a given user key
  private getUserNameByKey(userKey: string): any {
    this.firebaseController.getUserByKey(userKey).subscribe(a => {
      const data = a.payload.val();
      const id = a.key;
      this.assigneeArray.push(data['Name']);
    });
  }

  deArchiveUserstory(key){
    this.firebaseController.deArchiveUserstory(key);
  }
}

Test component: Archiveduserstoryoverview.component.spec.ts

import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ArchiveduserstoryoverviewComponent } from './archiveduserstoryoverview.component';
import {RouterTestingModule} from '@angular/router/testing';
import {FirebaseController} from '../../services/firebase-controller';
import {Observable, of} from 'rxjs';
import {AppModule} from '../../app.module';

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

  let fixtureUserstories = [
    {
      "-MFR0QIUc7tA3tAES5Zb" : {
        "AssignedUser" : "-MEZSC3KJvUynPd98kcH",
        "Description" : "",
        "EndDate" : "2020-02-22",
        "ProjectId" : "0",
        "SprintId" : "0",
        "Status" : "New",
        "Storypoints" : "",
        "Title" : "Frondend1"
      },
      "-MFRd7PsweHm07JUhZjA" : {
        "AssignedUser" : "",
        "Description" : "div links uitlijnen",
        "EndDate" : "2020-02-24",
        "ProjectId" : "0",
        "SprintId" : "-MFCHMDwfK84cVUdXosO",
        "Status" : "Archived",
        "Storypoints" : 1,
        "Title" : "About us fiksen"
      },
      "-MFRdG-biiv_okiRSWvi" : {
        "AssignedUser" : "-MEZSCn1yv9Yjo3eK4pr",
        "Description" : "nieuwe versie van angular",
        "EndDate" : "2020-02-25",
        "ProjectId" : "0",
        "SprintId" : "-MFCHMDwfK84cVUdXosO",
        "Status" : "New",
        "Storypoints" : 20,
        "Title" : "Updaten"
      }
    }
  ];
  let mockUserstories$ = of(fixtureUserstories);

  let fixtureUsers = [
    {
      "-MEZSC3KJvUynPd98kcH" : {
        "Name" : "Mitch"
      },
      "-MEZSCn1yv9Yjo3eK4pr" : {
        "Name" : "Maarten"
      },
      "-MEgdGlEPzDi1h32gTbH" : {
        "Name" : "John Doe"
      },
      "-MFYR3ln26SB8JjdE8eS" : {
        "Name" : "test"
      }
    }
    ]

  let mockUsers$ = of(fixtureUsers);

  beforeEach(async(() => {

    const fakeAFDB = jasmine.createSpyObj('FireBaseController', [ 'getUserstoriesSnapshot', 'getUserNameByKey']);

    fakeAFDB.getUserstoriesSnapshot.and.callFake(function() {
      return mockUserstories$;
    });

    fakeAFDB.getUserNameByKey('-MFYR3ln26SB8JjdE8eS').and.callFake(function() {
      return mockUsers$;
    });

    TestBed.configureTestingModule({
      imports: [
        RouterTestingModule, AppModule
      ],
      declarations: [ ArchiveduserstoryoverviewComponent ],
      providers: [ { provide: FirebaseController, useValue: fakeAFDB  }]
    })
      .compileComponents();
  }));

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

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

I start by creating Userstories and Users, the structure is just a snippet from the Firebase back-end.

Now when I run all my tests it gives these three errors:

I'm not sure what the "And" means in this instance, I used to think it was missing on the of the Methods that had to be called that I didn't stub or mock, but this seems to be false

Failed: Cannot read property 'and' of undefined

at <Jasmine>
    at UserContext.<anonymous> (http://localhost:9876/_karma_webpack_/src/app/views/archiveduserstoryoverview/archiveduserstoryoverview.component.spec.ts:75:54)
    at ZoneDelegate.invoke (http://localhost:9876/_karma_webpack_/node_modules/zone.js/dist/zone-evergreen.js:364:1)
    at AsyncTestZoneSpec.push../node_modules/zone.js/dist/zone-testing.js.AsyncTestZoneSpec.onInvoke (http://localhost:9876/_karma_webpack_/node_modules/zone.js/dist/zone-testing.js:1032:1)
    at ProxyZoneSpec.push../node_modules/zone.js/dist/zone-testing.js.ProxyZoneSpec.onInvoke (http://localhost:9876/_karma_webpack_/node_modules/zone.js/dist/zone-testing.js:289:1)
    at ZoneDelegate.invoke (http://localhost:9876/_karma_webpack_/node_modules/zone.js/dist/zone-evergreen.js:363:1)
    at Zone.runGuarded (http://localhost:9876/_karma_webpack_/node_modules/zone.js/dist/zone-evergreen.js:133:1)
    at runInTestZone (http://localhost:9876/_karma_webpack_/node_modules/zone.js/dist/zone-testing.js:1154:1)
    at UserContext.<anonymous> (http://localhost:9876/_karma_webpack_/node_modules/zone.js/dist/zone-testing.js:1092:1)
    at ZoneDelegate.invoke (http://localhost:9876/_karma_webpack_/node_modules/zone.js/dist/zone-evergreen.js:364:1)
    at ProxyZoneSpec.push../node_modules/zone.js/dist/zone-testing.js.ProxyZoneSpec.onInvoke (http://localhost:9876/_karma_webpack_/node_modules/zone.js/dist/zone-testing.js:292:1)

Really strange one, AngularFireDatabase isn't even made because I am completely mocking out my own controller

NullInjectorError: R3InjectorError(DynamicTestModule)[FirebaseController -> AngularFireDatabase -> AngularFireDatabase]: NullInjectorError: No provider for AngularFireDatabase!

error properties: Object({ ngTempTokenPath: null, ngTokenPath: [ 'FirebaseController', 'AngularFireDatabase', 'AngularFireDatabase' ] })
    at <Jasmine>
    at NullInjector.get (http://localhost:9876/_karma_webpack_/node_modules/@angular/core/__ivy_ngcc__/fesm2015/core.js:915:1)
    at R3Injector.get (http://localhost:9876/_karma_webpack_/node_modules/@angular/core/__ivy_ngcc__/fesm2015/core.js:11081:1)
    at R3Injector.get (http://localhost:9876/_karma_webpack_/node_modules/@angular/core/__ivy_ngcc__/fesm2015/core.js:11081:1)
    at injectInjectorOnly (http://localhost:9876/_karma_webpack_/node_modules/@angular/core/__ivy_ngcc__/fesm2015/core.js:801:1)
    at ɵɵinject (http://localhost:9876/_karma_webpack_/node_modules/@angular/core/__ivy_ngcc__/fesm2015/core.js:805:1)
    at Object.FirebaseController_Factory [as factory] (ng:///FirebaseController/ɵfac.js:5:46)
    at R3Injector.hydrate (http://localhost:9876/_karma_webpack_/node_modules/@angular/core/__ivy_ngcc__/fesm2015/core.js:11248:1)
    at R3Injector.get (http://localhost:9876/_karma_webpack_/node_modules/@angular/core/__ivy_ngcc__/fesm2015/core.js:11070:1)
    at NgModuleRef$1.get (http://localhost:9876/_karma_webpack_/node_modules/@angular/core/__ivy_ngcc__/fesm2015/core.js:24198:1)
    at Object.get (http://localhost:9876/_karma_webpack_/node_modules/@angular/core/__ivy_ngcc__/fesm2015/core.js:22101:1)

This one makes sense, the test doesn't pass

Expected undefined to be truthy.

Error: Expected undefined to be truthy.
    at <Jasmine>
    at UserContext.<anonymous> (http://localhost:9876/_karma_webpack_/src/app/views/archiveduserstoryoverview/archiveduserstoryoverview.component.spec.ts:96:23)
    at ZoneDelegate.invoke (http://localhost:9876/_karma_webpack_/node_modules/zone.js/dist/zone-evergreen.js:364:1)
    at ProxyZoneSpec.push../node_modules/zone.js/dist/zone-testing.js.ProxyZoneSpec.onInvoke (http://localhost:9876/_karma_webpack_/node_modules/zone.js/dist/zone-testing.js:292:1)

I am so incredibly stuck on this I have rewritten the way I mock multiple times and I really cannot seem to solve it, if anyone please has the time to give me a hand? Let me know if you'd like to see any other files.

Upvotes: 0

Views: 2160

Answers (1)

Wesley Trantham
Wesley Trantham

Reputation: 1264

Your biggest issue is that you're importing AppModule. Your app module is going to bring in all of your module's dependencies - you should only import what this particular component needs, which is exclusively the RouterTestingModule.

Once you remove that you should no longer get an error about AngularFireDatabase. What was likely to be happening is that since AppModule was providing the real FirebaseController service it was using the one from AppModule first.

Next issue - in your component you have a method named getUserNameByKey and it calls this.firebaseController.getUserByKey. Your fakeAFDB should have getUserByKey.

Third issue -

fakeAFDB.getUserNameByKey('-MFYR3ln26SB8JjdE8eS').and.callFake(function() {
      return mockUsers$;
    });

Notice how you're calling the function here? With any fake you are defining the function, not invoking it. The way to do this is fakeAFDB.getUserByKey.and.callFake(function () { return mockUsers$; });

Now, written this way you will be returning the exact same value no matter what is passed in. If you want to do logic on that you can do something like

fakeAFDB.getUserByKey.and.callFake(function (key) {
  if (key = 'some string'){
    return mockUsers$;
  }
  return someOtherResult;
});

Here is the final solution you're looking for. This will still throw errors, however, after you call fixture.detectChanges as the code in your controller's ngOnInit looks at if(item.payload.val()['ProjectId'] == this.projectId){ and your getUserstoriesSnapshot result doesn't have a val function defined. I trust that you can modify your fake results accordingly.

beforeEach(async(() => {
    const fakeAFDB = jasmine.createSpyObj<FirebaseController>('FireBaseController', ['getUserstoriesSnapshot', 'getUserByKey']);
    // returnValue is better here since you don't need logic on the return
    fakeAFDB.getUserstoriesSnapshot.and.returnValue(mockUserstories$);

    fakeAFDB.getUserByKey.and.callFake(() => {
      return mockUsers$;
    });

    TestBed.configureTestingModule({
      declarations: [ArchiveduserstoryoverviewComponent],
      imports: [RouterTestingModule],
      providers: [ { provide: FirebaseController, useValue: fakeAFDB  }]
    })
      .compileComponents();
  }));

Upvotes: 1

Related Questions