<img height="1" width="1" style="display:none" src="https://www.facebook.com/tr?id=1063935717132479&amp;ev=PageView&amp;noscript=1 https://www.facebook.com/tr?id=1063935717132479&amp;ev=PageView&amp;noscript=1 "> Bitovi Blog - UX and UI design, JavaScript and Front-end development
Loading

Angular |

How to Write Angular Unit Tests That Actually Work

Optimize your Angular unit tests when using HttpClient. Learn how to properly configure your mocks to avoid false positives and catch subtle issues.

Mark Thompson

Mark Thompson

Twitter Reddit

Most applications leverage API requests to fetch data. Considering data fetching is crucial for Angular testing. Consequently, it's common to include HttpClient in your unit tests.

Consider the following code:

/** /src/app/services/posts/posts.service.ts */

@Injectable({
  	providedIn: 'root'
})
export class PostsService {
  	constructor(private httpClient: HttpClient) {}

	getPosts(): Observable<Post[]> {
		return this.httpClient.get<Post[]>('<https://some-api.com/posts');
	}
}
/** /src/app/components/posts/posts.component.ts */

@Component({/* ... */})
export class PostsComponent {
	posts: Post[] = [];
  	hasPosts = false;

	constructor(private postsService: PostsService) {}

  	getPosts(): void {
		this.postsService.getPosts().subscribe(posts => {
			this.posts = posts;
		});

	    // πŸ‘‡ This if statement is questionable... πŸ€”
		if (this.posts.length > 0) {
			this.hasPosts = true;
		}
	}
}

The intention for getPosts() is to fetch posts from some server, and the component renders a special message if no posts are available using hasPosts.

But, there is a glaring issue with this code. The check for posts.length is outside the subscribe callback and will always return 0, and hasPosts will always be false.

The fix is obvious, but that’s not what this blog post highlights: shouldn't a simple problem like this be easily detected through unit testing? Let's find out.

Writing Your Unit Test

When writing tests involving HttpClient, you have 2 main options:

  1. mock HttpClient using HttpTestingController provided by HttpClientTestingModule.

  2. mock the service that depends on HttpClient by stubbing methods that would normally make a request.

The latter option tends to be the common choice since it requires the least amount of code to set up and is a one-size-fits-all pattern you can use outside of HttpClient-related implementations.

Let's set up your unit test:

/** /src/app/components/posts/posts.component.spec.ts */

import { ComponentFixture, TestBed } from '@angular/core/testing';
import { of, Observable } from 'rxjs';
import { PostsComponent } from './posts.component';
import { Post, PostsService } from '../../services/posts.service';

// πŸ‘‡ A mock instance of PostService
const mockPostsService = {
  	// Instead of making an API request,
	// always return an array of 2 Post objects
	getPosts: (): Observable<Post[]> => of([
		{ id: 1, title: 'first post'},
		{ id: 2, title: 'second post' },
	]),
} as PostsService;

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

  	beforeEach(async () => {
		await TestBed.configureTestingModule({
			declarations: [PostsComponent],
			providers: [
				{
					provide: PostsService,
      			    // πŸ‘‡ Provide the mocked instance of PostsService
					useValue: mockPostsService,
				}
			]
		}).compileComponents();
	});

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

You'll notice a mocked instance of PostsService in your spec file and provided it using our TestBed. Now you can write your test:

/** /src/app/components/posts/posts.component.spec.ts */

describe("getPosts()", () => {
	it('should set hasPosts to true if there are posts', () => {
		component.getPosts();
		expect(component.posts.length).toBe(2);
		expect(component.hasPosts).toBe(true);
	});
});

Here is the basic test that should catch the problem with the code. Let's run it:

1 spec, 0 failures

PostsComponent
  	getPosts()
	βœ… should set hasPosts to true if there are posts

The test should have failed since hasPosts shouldn't be set to true correctly. Why does this broken Angular test pass? Is this a bug with Jasmine?

Observables Can Be Asynchronous & Synchronous

The root of this confusion revolves around the common misunderstanding about how scheduling works for RxJS Observables. Unlike Promises, Observables can be both asynchronous and synchronous.

To verify this behavior, consider this code snippet:

import { of } from 'rxjs';

setTimeout(() => {
  	console.log('(1) setTimeout');
});

Promise.resolve().then(() => {
  	console.log('(2) Promise');
});

of('(3) Observable').subscribe(value => console.log(value));

console.log('(4) Just console.log');

The order of the console logs are:

(3) Observable
(4) Just console.log
(2) Promise
(1) setTimeout

This is because the subscribe callback from of() uses synchronous scheduling by default. This results in (3) Observable being logged immediately and not delayed like Promises or setTimeout().

API requests are always asynchronous, so the mocked PostsService should reflect this in its getPosts method. Luckily, RxJS provides the scheduler utility to override the default scheduling of Observables:

/** /src/app/components/posts/posts.component.spec.ts */

import { ComponentFixture, TestBed } from '@angular/core/testing';
import { asyncScheduler, scheduled, of, Observable } from 'rxjs';
// πŸ‘† Import asyncScheduler and scheduled from 'rxjs'
import { PostsComponent } from './posts.component';
import { Post, PostsService } from '../../services/posts.service';

// A mock instance of PostService
const mockPostsService = {
  	getPosts: (): Observable<Post[]> => scheduled(of([
		{ id: 1, title: 'first post'},
		{ id: 2, title: 'second post' },
	]), asyncScheduler),
  	// πŸ‘† Specifying that we want our stream to be asynchronous
} as PostsService;

Now with this refactor, tests will properly catch the problem in the code.

1 spec, 1 failures

PostsComponent
  	getPosts()
		❌ should set hasPosts to true if there are posts

		Expected 0 to be 2.
		Expected false to be true.

Conclusion

The moral of the story is to be cautious about how you implement mocks. Mocks can result in false positives in your unit tests when you aren't mindful of how scheduling works with your real implementation.

What do you think?

Are you following best practices when testing your Angular application? Want a second opinion? Join our Community Discord and share your feedback with us on the #Angular channel. Don't hesitate to schedule a free consultation call if you need any assistance. Our team is always here to support you.

Join our Discord