Search code examples
angularunit-testingtestingrxjsjasmine

Unit testing a compnent that is getting data from Angular Replay Subject


I have this service called EmployeeService where I am using Angular's ReplaySubject to get data from an api. I'm subcribing to this ReplaySubject from another component which I am trying to test now. But because I'm unable to mock the data from the service mentioned above, the unit test is not working.

This is a bare bones of the EmployeeService:

class EmployeeService extends ApiService {

  public currentEmployeeSub: ReplaySubject<EmployeeModel> = new ReplaySubject<EmployeeModel>(1);
  private currentEmployee: EmployeeModel;
  private uuid: UuidService;

  constructor(
    auth: AuthService,
    http: HttpClient,
    uuid: UuidService
  ) {
    super('employees', auth, http);
    this.uuid = uuid;
  }

  // other methods
}

This is a utilService which is also being used to parse the data:

class UtilService {
  parse(object: any) {
    return JSON.parse(JSON.stringify(object));
  }
}

Now this is the component I'm trying to test. Please note that this is a huge component but I'm just showing the most important part which is needed for this component to work and that is the employee data coming from EmployeeService.

class SalaryComponent implements OnInit, OnDestroy, AfterViewInit {

  private utilService: UtilService;
  private employeeService: EmployeeService;

  employee: EmployeeModel;
  employeeSubscription: Subscription;

  constructor(utilService: UtilService, employeeService: EmployeeService
  ) {
    this.utilService = utilService;
    this.employeeService = employeeService;

    this.employeeSubscription = this.employeeService.currentEmployeeSub.subscribe((employee: EmployeeModel) => {
      this.employee = this.utilService.parse(employee);
    });
}

The above component is subscribing to ReplaySubject in EmployeeService, gets the data of the employee and sets this.employee after parsing the data. I just need to set the data for this.employee in the test spec so that I can unit test a component which relies on this data like this:

<div class="cb-col-12">
   <dropdown
      *ngIf="employee"
      name="employee"
      formControlName="employee"
      (handlerSelect)="onEmployeeChange($event)"
      [options]="employeeOptions"
   >
   </dropdown>
</div>

This is how the test spec look like:

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

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      imports: [],
      declarations: [],
      providers: [],
    })
      .compileComponents();
  });

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

  fit('should show drop down if there is employee data', async ()  => {
    fixture.detectChanges();
    await fixture.whenStable();
  
    component.employee = {
      name: 'John Doe',
      age: 23,
      qualifications: [],
      address: {},
    }

    fixture.detectChanges();
    await fixture.whenStable();

    const hostElement: HTMLElement = fixture.nativeElement;
    const dropDown: HTMLInputElement = hostElement.querySelector(
      'dropdown[name="employee"]'
    );

    expect(dropDown).toBeTruthy();
  } 
}

I am getting null for the dropdown element even when there is employee data set in the component.

How can I mock ReplaySubject to set employee data in the component so that it takes effect?


Solution

  • You have to mock the EmployeeService for it to have the data you want.

    This is not a complete answer but should hopefully get you started.

    Follow comments with !!:

    describe('SalaryComponent', () => {
      let component: SalaryComponent;
      let fixture: ComponentFixture<SalaryComponent>;
      // !! Declare a mock for EmployeeService
      let mockEmployeeService: jasmine.SpyObj<EmployeeService>;
    
      beforeEach(async () => {
        // !! Assign the mock
        mockEmployeeService = jasmine.createSpyObj<EmployeeService>('EmployeeService', {}, { currentEmployeeSub: of({ /* Mock Employee model here */ } as any) });
        // !! Look into jasmine.createSpyObj, the first string argument is optional 
        // and for debugging purposes. The 2nd argument can be an object or an array
        // where you can mock public methods and the 3rd argument is an object
        // where you can mock public instance variables as we did here.
        await TestBed.configureTestingModule({
          imports: [],
          declarations: [],
          providers: [
            // !! Provide the mock
            { provide: EmployeeService, useValue: mockEmployeeService }, 
          ],
        })
          .compileComponents();
      });
    
      beforeEach(() => {
        fixture = TestBed.createComponent(SalaryComponent);
        component = fixture.componentInstance;
      });
    
      fit('should show drop down if there is employee data', async ()  => {
        fixture.detectChanges();
        await fixture.whenStable();
      
        component.employee = {
          name: 'John Doe',
          age: 23,
          qualifications: [],
          address: {},
        }
    
        fixture.detectChanges();
        await fixture.whenStable();
    
        const hostElement: HTMLElement = fixture.nativeElement;
        const dropDown: HTMLInputElement = hostElement.querySelector(
          'dropdown[name="employee"]'
        );
    
        expect(dropDown).toBeTruthy();
      } 
    }