Wednesday 16 August 2023

Angular Jasmine Unit Test - Child Component with Input and Output Properties

We are going to write some unit tests for an Angular component that calls child components. Our main component template is as follow:
. . .
. . . some content of the main component . . .
. . .
<ng-container *ngFor="let id of studentIds">
  <app-student-profile [studentId]="id"  (alertEmitter)="displayAlert($event)></app-student-profile>
</ng-container>
. . .
Our child component:
<div *ngIf="student" class="studentDetails">
  <button id="btnTest" (click)="sendAlert()">send alert</button>
  <div>
    <div>Student ID</div>
    <div>{{student.studentId}}</div>
  </div>
  <div>
    <div>First Name</div>
    <div>{{student.firstName}}</div>
  </div>
  <div>
    <div>Last Name</div>
    <div>{{student.lastName}}</div>
  </div>
  <div>
    <div>Email Address</div>
    <div>{{student.email}}</div>
  </div>
</div>
The child component also has input and output properties. The input is expecting Student ID to be passed from the parent component and the output will pass a message to the parent component to be displayed.

Some of the codes from child component class:
. . .
@Input() studentId!: number;
@Output() alertEmitter: EventEmitter<string> = new EventEmitter<string>();

sendAlert(): void {
  this.alertEmitter.emit("an alert from student profile component with Student Id: " + this.studentId);
}
. . .

To test the child component, we can use ng-mocks testing library, which is popularly used for Angular testing. Our tests will look like:
describe('MainComponent', () => {
  let component: MainComponent;
  let childComponent: StudentComponent;
  let fixture: ComponentFixture<MainComponent>;

  beforeEach(async () => {

    await TestBed.configureTestingModule({
      declarations: [MainComponent, 
	                 MockComponent(StudentComponent)],
      //schemas: [NO_ERRORS_SCHEMA]
    })
    .compileComponents();

    fixture = TestBed.createComponent(MainComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

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

  it("should have correct numbers of <app-student-profile> child component(s)", () => {
    let childComponents = fixture.debugElement.queryAll(By.directive(StudentComponent));
    expect(childComponents.length).toEqual(studentIds.length)
  })

  it("should pass right argument to child components", () => {
    let childComponents = fixture.debugElement.queryAll(By.directive(StudentComponent));
    for (var i = 0; i < component.studentIds.length; i++) {
      let childComponent: StudentComponent = childComponents[i].componentInstance;
      expect(childComponent.studentId).toEqual(component.studentIds[i]);
    }
  })
});
On line 10, we mock the child component with MockComponent().

[NO_ERRORS_SCHEMA] is also not needed in the declaration like in other approaches without using ng-mocks library.

On line 25, we use fixture.debugElement.queryAll(By.directive(CHILD_COMPONENT_NAME)) to find all child components. Notice that with the library, we can find the child component by class name (type). Other approaches need to use a fake component class or querying the html element (using By.css() function).

Line 32, we get the child component object with .componentInstance. Then we will be able to access all its properties and methods. We can check the argument passed to its input property by directly inspecting its class property.


Lastly, we need to test the output property. It will relay an event then call this function on parent component:
displayAlert(message: string): void {
  console.log(message);
}
We can test this interaction with something like:
it("should be able to catch alert from child component", () => {
  const alertMessage: string = "test alert";
  spyOn(console, 'log');
  //spyOn(component, 'displayAlert');   // if we want to test the parent component function is called
  epProfileComponent = fixture.debugElement.query(By.directive(StudentEPProfileComponent)).componentInstance;
  epProfileComponent.alertEmitter.emit(alertMessage);
  //expect(component.displayAlert).toHaveBeenCalledWith(alertMessage);   // if we want to test the parent component function is called
  expect(console.log).toHaveBeenCalledWith(alertMessage);
})
Notice on line 6, we can call the emit() function and then on line 8, check that the parent's function we want to be called is called (in our example is console.log).

Friday 11 August 2023

Angular Jasmine Unit Test - Faking Service and its Methods

On this post, we will try to create Jasmine unit tests to fake a service and its function that is used in an Angular component.
Used Angular version is 15 and Jasmine is 4.5.0.

Our component page:
export class HomeComponent implements OnInit {
  student?: StudentWithBasicProfile;

  constructor(
    private studentService: StudentService,
    private route: ActivatedRoute) { }

  ngOnInit(): void {
    let studentId = this.route.snapshot.params['id'];

    this.studentService.getStudentBasicProfile(studentId)
      .subscribe(student => {
          this.student = student;
      });
  }  
}
This component retrieves an id from query string then call a method of a service then display the result.

Our tests look like this:
describe('HomeComponent', () => {
  let component: HomeComponent;
  let fixture: ComponentFixture<HomeComponent>;
  let studentServiceSpy: jasmine.SpyObj<StudentService>;
  let response: StudentWithBasicProfile;
  let routeId: number = 123;

  beforeEach(async () => {
    studentServiceSpy = jasmine.createSpyObj('StudentService', ['getStudentBasicProfile']);
    response = {
      studentId: routeId,
      firstName: '',
      lastName: '',
      email: '',
      mobilePhone: ''
    };
    studentServiceSpy.getStudentBasicProfile.and.returnValue(of(response));

    await TestBed.configureTestingModule({
      declarations: [HomeComponent],
      providers: [
        { provide: StudentService, useValue: studentServiceSpy },
        { provide: ActivatedRoute, useValue: { snapshot: { params: { 'id': routeId } } } }  // this one is to fake "this.route.snapshot.params['id']" code
      ]
    })
    .compileComponents();

    fixture = TestBed.createComponent(HomeComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

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

  it("should call getStudentBasicProfile() with correct parameter and return data", () => {
    expect(studentServiceSpy.getStudentBasicProfile).toHaveBeenCalledTimes(1);
    expect(studentServiceSpy.getStudentBasicProfile).toHaveBeenCalledWith(routeId);
    expect(component.student).toEqual(response);
  });
});
On lines 4, 9, and 17, we set up a fake StudentService and its function 'getStudentBasicProfile' and set up a return value. Then on line 22, we use this fake service instead of the real service.

On line 23, we fake the query string value in route with "{ snapshot: { params: { 'id': [VALUE] } } }".