angulartestingdependency-injectionkarma-jasmine

Angular test Class with service dependency


I'm trying to test a plain class (not a component) in Angular which has a service dependency and a separate constructor argument. I've mirrored the docs for component testing with a dependency, but I get the following error:

Unexpected value 'MyClass' imported by the module 'DynamicTestModule'. Please add a @NgModule annotation.

my-service.service.ts:

@Injectable({
    providedIn: 'root',
})
export class MyService { ... }

my-class.ts:

import { inject } from '@angular/core';
import { MyService } from './my-service.service';

export class MyClass {
    private _myService: MyService = inject(MyService);
    private _name: string;

    constructor(name: string) {
        this._name = name;
    }
}

my-class.spec.ts

import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MyService } from './my-service.service';
import { MyClass } from './my-class';

describe('MyClass', () => {
    let myClass: MyClass;
    let mockMyService: Partial<MyService>;
    let fixture: ComponentFixture<MyClass>;

    beforeEach(() => {
        mockMyService = {};

        TestBed.configureTestingModule({
            imports: [MyClass],
            providers: [{ provide: MyService, useValue: mockMyService }],
        });

        fixture = TestBed.createComponent(MyClass); // causes error
        myClass = fixture.componentInstance;

        TestBed.inject(MyService);
    });

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

updated my-class.spec.ts

import { MyClass } from './my-class';

describe('MyClass', () => {
    let myClass: MyClass;

    beforeEach(() => {
        myClass = new MyClass('someName');
    });

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

How can I test this plain class with the service dependency?


Solution

  • Since you are using MyClass as a component, you need the @Component decorator to initialize it as a component so we can create a dummy class with the component decorator and inherit all the contents of MyClass into the testing class MyClassMocked.

    Since you are using MyClass in the imports array, that means, it must be standalone: true so I have added that, if you do not want standalone as true then you need to add MyClass to the declarations array instead!

    Also we need to update the class MyClass to MyClassMocked wherever it's used as a type:

    import { ComponentFixture, TestBed } from '@angular/core/testing';
    import { MyService } from './my-service.service';
    import { MyClass } from './my-class';
    
    import { inject, Component } from '@angular/core';
    import { MyService } from './my-service.service';
    
    @Component({ selector: 'my-class', standalone: true, template: '' })    
    export class MyClassMocked extends MyClass {
         constructor() { super(); }
    }
    
    describe('MyClass', () => {
        let myClass: MyClassMocked;
        let mockMyService: Partial<MyService>;
        let fixture: ComponentFixture<MyClassMocked>;
    
        beforeEach(() => {
            mockMyService = {};
    
            TestBed.configureTestingModule({
                imports: [MyClassMocked],
                providers: [{ provide: MyService, useValue: mockMyService }],
            });
    
            fixture = TestBed.createComponent(MyClassMocked); // should work!
            myClass = fixture.componentInstance;
    
            TestBed.inject(MyService);
        });
    
        it('should create', () => {
            expect(myClass).toBeTruthy();
        });
    });