Welcome back to the second installment of “Tips for Unit Testing in Front-end Frameworks.” In the final part of this series, we’ll continue writing unit tests for a basic toy authentication component. We’ll apply the lessons learned in Part 1, including determining our testing prerequisites. This installment will also cover some organizational best practices and introduce common Jest methods that are helpful for unit testing.
Test Setup
Writing tests for a component using Jest requires a bit of setup. In order to test our authentication component from Part 1, we need to give our testing environment access to it. To do this, we first create a variable named ‘component’ of the type UserAuthenticationComponent. We will then use something called the Angular ComponentFixture. ComponentFixture creates a wrapper of sorts around our component instance in the DOM and allows us to do things such as modify component properties and call methods. It can also be used to query and manipulate the DOM. We’ll start by creating another variable named ‘fixture’ of the type ComponentFixture<UserAuthenticationComponent>. Next, we’ll use the Angular TestBed to set up and create our component and assign it to our fixture variable. Typically, TestBed setup should be done in a beforeEach() hook so that the component instance is properly initialized for each test.
describe('UserAuthenticationComponent', () => {
let component: UserAuthenticationComponent;
let fixture: ComponentFixture;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [UserAuthenticationComponent, HttpClientModule]
})
.compileComponents();
fixture = TestBed.createComponent(UserAuthenticationComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
//UserAuthenticationComponent Tests continue here...
As you can see in the beforeEach hook in the above code block, we use the TestBed method configureTestingModule() to create an Angular module specifically designed as a testing environment. We’ve given it our component as an import, as well as the HttpClientModule we’re using in our component. After compiling the components, we create the component and assign it to our fixture variable. To our component variable we assign the componentInstance attached to our fixture. Finally, we tell Angular to signal change detection with fixture.detectChanges(), and we’re ready to test our component’s logic.
Organizing Your Test File
Now that we’re able to write our tests, let’s discuss organizing your test file. While you can nest ‘describe’ blocks, you can’t do so for ‘it’ blocks, so your ‘it’ blocks should hold the most atomic, basic versions of your unit tests. Utilizing nested ‘describe’ blocks can also make it much easier to debug tests in the future, as they will allow future-you (or a teammate) to quickly narrow down the functionality that is broken. For the toy authentication component being used in this example we only have a couple basic tests, so this is less vital than in large-scale projects. Let’s look at the organization I chose for our tests.
describe('UserAuthenticationComponent', () => {
let component: UserAuthenticationComponent;
let fixture: ComponentFixture;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [UserAuthenticationComponent, HttpClientModule]
})
.compileComponents();
fixture = TestBed.createComponent(UserAuthenticationComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
//UserAuthenticationComponent Tests continue here...
describe('updatePassword', () => {
it('should display success message when request succeeds', () => {
//Test Body Goes Here
});
it('should display error message when request fails', () => {
//Test Body Goes Here
});
})
});
Within our UserAuthenticationComponent ‘describe’ block, I created a nested ‘describe’ block for the specific method I wanted to test: updatePassword(). Within that ‘describe’ block, I laid out our two basic tests in separate ‘it’ blocks. (Note: in each ‘it’ block, you should really only be asserting on one general outcome – this will keep your tests clean and organized for the future).
Writing Tests to Assert on an Outcome
Finally, let’s actually get to testing the method we’ve chosen within our toy authentication component. We’ll start with the test to display a success message when the request succeeds.
public updatePassword(newPassword: string): void {
this.sendPasswordUpdateRequest(newPassword).subscribe({
next: (res: HttpResponse) => {
alert("Password updated successfully!");
},
error: (err: any) => {
console.error('Error updating password:', err);
alert("Failed to update password.");
}
});
};
This is where our testing ‘prerequisites’ that we discussed in the previous installment of this series come into play. As we discovered in Part 1, we know that updatePassword() sends a patch HTTP request to update a user’s password. If the request completes without error, we create an alert for the user with the message “Password updated successfully!” If our request completes in error, we alert the user with the message “Failed to update password.” This is the branching logic that we want to test.
Writing the ‘Success Test’
For our first test, which I’ll refer to as the success test, we can glean from our production code that we will need a mock HTTP request, and since we use .subscribe(), we know that we will need it as an observable. We accomplish this with the following syntax.
it('should display success message when request succeeds', () => {
//Arrange
const mockSuccessResponse = new HttpResponse;
const mockSuccessResponseObservable = of(mockSuccessResponse);
// Continue with arrangement and test here ...
});
Now that we have our mock HTTP response, we return to our source code and see that this observable is being returned by the method sendPasswordUpdateRequest() called in updatePassword(). This tells us that we need to find a way to make this method return our ‘fake’ response. Luckily, we have a simple solution with jest.spyOn(component, method).mockReturnValue(). This is where our initial setup pays off, as we have our component instance that we can easily pass into this function, along with the method that we wish to spy on. From there, we can use mockReturnValue() with our mocked HTTP response observable to fake the return of a successful HTTP response from sendPasswordUpdateRequest(). We also create a hard coded fake password string that we will need to call our method updatePassword().
But wait! If we want to assert on the message that is being sent after updatePassword() receives the response observable, we need a way to read into what the alert() function is doing. We can use jest.spyOn() to accomplish this task as well. However, this time around we don’t want to mock the return value since that is what we hope to assert on. We can spy on the global object ‘window’ and its method ‘alert()’ that updatePassword() calls.
it('should display success message when request succeeds', () => {
//Arrange
const mockSuccessResponse = new HttpResponse;
const mockSuccessResponseObservable = of(mockSuccessResponse);
const sendPasswordUpdateRequestSpy = jest.spyOn(component, 'sendPasswordUpdateRequest').mockReturnValue(mockSuccessResponseObservable);
const newPassword = "test1";
const alertSpy = jest.spyOn(window, 'alert');
//Act
component.updatePassword(newPassword);
//Assert
expect(sendPasswordUpdateRequestSpy).toHaveBeenCalledTimes(1);
expect(alertSpy).toHaveBeenCalledWith('Password updated successfully!');
});
Acting and Asserting
Now that we’ve created our alertSpy and sendPasswordUpdateRequestSpy (returning our mockSuccessResponseObservable), we can finally call the updatePassword() method with our newPassword input. We do this by calling the method on our component instance.
Finally, we can assert on the expected outcome we had using Jest functionality. Here, I decided to first make sure that the sendPasswordUpdateRequestSpy was called once using expect().toHaveBeenCalledTimes(n). This tells me that updatePassword() did actually call sendPasswordUpdateRequest() with our mocked response. I also decided to assert on our original expectation, which was that the alertSpy was called with the success message we expected by using expect().toHaveBeenCalledWith().
Writing the ‘Failure Test’
We can repeat this method with a couple of changes for the failure test we outlined earlier. The key differences:
- We need to create a mock HTTP error response.
- When we call mockReturnValue() on our spy for sendPasswordUpdateRequest(), the return value needs to be a thrown error. This is due to the fact that the error message is triggered by a caught error in updatePassword().
- We should expect that our alertSpy is called with the error message, ‘Failed to update password’.
See the below code block for the completed failure test:
it('should display error message when request fails', () => {
//Arrange
const mockErrorResponse = new HttpErrorResponse({
error: "Your password change request has failed.",
status: 400,
statusText: "",
});
const sendPasswordUpdateRequestSpy = jest.spyOn(component, 'sendPasswordUpdateRequest').mockReturnValue(throwError(mockErrorResponse));
const newPassword = "test1";
const alertSpy = jest.spyOn(window, 'alert');
//Act
component.updatePassword(newPassword);
//Assert
expect(sendPasswordUpdateRequestSpy).toHaveBeenCalledTimes(1);
expect(alertSpy).toHaveBeenCalledWith('Failed to update password.');
});
A Foundation for Unit Testing in Front-End Frameworks
In this final installment of Tips for Unit Testing in Front-end Frameworks, we tackled a variety of best practices for testing in Jest and applied the methodology of determining our “testing prerequisites” from Part 1 to write actual tests for a toy authentication component. We covered basic setup using the Angular TestBed and ComponentFixture, as well as helpful Jest methods you can use to arrange and assert in your tests: spyOn() and mockReturnValue() for arrangement, as well as toHaveBeenCalledTimes() and toHaveBeenCalledWith() for assertion. Although front-end testing can take a while to get the hang of, my goal for this series is to provide a solid foundation for understanding what goes into writing tests in front-end frameworks and introduce a helpful framework for determining how to organize and write effective and long-lasting unit tests.