0

I'm trying to write my first 'complex' test against Angular using Jasmine and Karma. This is the code that I'm trying to write a test against:

import { Component, Inject } from "@angular/core";
import { FormGroup, FormControl, FormBuilder, Validators } from '@angular/forms';
import { Router } from "@angular/router";
import { AuthService } from '../../services/auth.service';

@Component({
   selector: "login",
   templateUrl: "./login.component.html",
   styleUrls: ['./login.component.css']
})

export class LoginComponent {

title: string;
form!: FormGroup;

constructor(private router: Router,
    private frmbuilder: FormBuilder,
    private authService: AuthService,
    @Inject('BASE_URL') private baseUrl: string) {

    this.title = "User Login";

    this.createForm();
}

createForm() {
    this.form = this.frmbuilder.group({
        Email: ['', Validators.required],
        Password: ['', Validators.required]
    });
}…

I'm trying to write a test for the createForm method:

import { TestBed, async } from '@angular/core/testing';
import { LoginComponent } from './login.component';
import { Router } from '@angular/router';
import { AuthService } from '../../services/auth.service';
import { FormGroup, FormControl, FormBuilder, Validators } from '@angular/forms';


describe('create form', () => {

  let component : LoginComponent;
  let service : AuthService;
  let router: Router;
  let frmBuilder: FormBuilder;
  let spy : any;

beforeEach(() => {
    spyOn(component, LoginComponent.arguments(router, frmBuilder, service));
    component = new LoginComponent(router, frmBuilder, service, '');

});


it('expect the login form to have been created', () => {
    expect(component.createForm()).toHaveBeenCalled();
});

});

When I run the test I receive the following error: TypeError: 'caller', 'callee', and 'arguments' properties may not be accessed on strict mode functions or the arguments objects for calls to them.

The link offered as an answer doesn't solve my issue. I think it's because I'm calling my createForm method in the constructor (perfectly valid, but hard to test). If I change the first line of my test in the beforeEach to be : spyOn(component,'createForm'); then I receive an error could not find an object to spy upon for createForm()

bilpor
  • 3,467
  • 6
  • 32
  • 77
  • 1
    Possible duplicate of [Spying on a constructor using Jasmine](https://stackoverflow.com/questions/9347631/spying-on-a-constructor-using-jasmine) – Gordon Westerman Oct 26 '18 at 09:00
  • That really isn't a good strategy. If you want to test that creating the component creates a form, then create the component, and check that once created, it has a form. There's no need to spy on anything to do that. – JB Nizet Oct 26 '18 at 09:28
  • @JBNizet I thought I'd need a spy because of all the components in my constructor. I don't want to have to pass concrete instances of those. Can you suggest what my test should look like, I'm fairly new to Jasmine – bilpor Oct 26 '18 at 09:31
  • I don't understand why someone is voting to close my question. It's a genuine problem for me – bilpor Oct 26 '18 at 09:37
  • You can do this by using the angular `TestBed` to create and compile your component. `TestBed` basically mimics an angular module. Check out the docs [here](https://angular.io/guide/testing) – Bon Macalindong Oct 26 '18 at 09:37
  • Then you shouldn't spy on the constructor. You should create mock instances of the arguments (i.e. mock instances of Router and of AuthService), and these mock instances as arguments when calling the constructor. The FormBuilder should really not be mocked. Create a real one (by calling its constructor, or asking the TestBed to provide one). You can also ask for real instances of the aother services to the TestBed, and spy on them if necessary, BTW. – JB Nizet Oct 26 '18 at 09:38
  • Ok, if in my beforeEach I simply put `spy = TestBed.createComponent(loginComponent)` and then change my expect statement to be `expect(spy.createForm()).tohavebeencalled();` I receive an error `could not load the summary for directive LoginComponent` – bilpor Oct 26 '18 at 09:50
  • Using the TestBed for isolated unit tests is not warranted in your case. You only really need it in order to create shallow or integration tests. Here is [an interesting article](https://vsavkin.com/three-ways-to-test-angular-2-components-dcea8e90bd8d) on the topic. – Gordon Westerman Oct 26 '18 at 11:16

1 Answers1

0

In order to spy upon a method you need an instance of the class (system under test or sut) the method belongs to so the method can be replaced with a spy.

spyOn<any>(sut, 'initializeForm');

This means that the class in question must have been instantiated before you spy upon its methods or properties which is easy to do for spy that objects you pass to the constructor in your beforeEach method or for methods which belong to the sut that are not called by the constructor itself but afterwards.

But since you want to check if the constructor is calling the method, but you need an instance of that class before calling the constructor, in order to spy upon a method that is called by the constructor, but instantiating the class without its constructor is not possible, you have run into a dilemma.

My advice would be to move your method call to the ngOnInit method and test that.

describe('constructor', () => {
    it('should create the sut', () => {
        expect(sut).toBeDefined();
    });
});

describe('ngOnInit', () => {
    it('should call initializeForm', () => {
        spyOn<any>(sut, 'initializeForm');

        sut.ngOnInit();

        expect(sut.initializeForm).toHaveBeenCalled();
    });
});

describe('initializeForm', () => {
    it('should initialize the form', () => {
        sut.initializeForm();

        expect(sut.form).toBeDefined();
    });
});

Yes, simple forms can be created in your constructor. But as things get more complex, for instance when you want to access @Input parameter values, the ngOnInit method is the best place to start form creation.

In order to keep Angular from throwing templating errors either provide a dumbed down/empty version of your form in your constructor or keep the HTML from rendering until ngOnInit has run through and the form is defined using *ngIf.

<form *ngIf="form" [formGroup]="form">
  ...
</form>
Gordon Westerman
  • 1,046
  • 10
  • 12