#Unit testing service with dependencies

6 messages · Page 1 of 1 (latest)

steady mortar
#

Hello guys. Can someone review my unit tests?
I'm a beginner to unit tests and for now this doesn't make sense to me, but maybe it's not so bad 😄

import { TestBed } from "@automock/jest";
import { BadRequestException } from "@nestjs/common";

const mockBook: Book = {} // some object with mock data

const mockBooks: Book[] = [mockBook, mockBook];

describe("Book service", () => {
  let service: BooksService;
  let getBookByBookIdApi: jest.Mocked<GetBookByBookIdApi>;

  const getBookQuery: GetBookQuery = {
    brand: "bookBrand",
    bookId: "oaeifn123aeklgfn",
  };

  beforeAll(() => {
    const { unit, unitRef } = TestBed.create(BooksService).compile();

    service = unit;
    getBookByBookIdApi = unitRef.get(GetBookByBookIdApi);

    jest.clearAllMocks();
  });

  it("Should be defined", () => {
    expect(service).toBeDefined();
  });

  describe("getBookByBookId", () => {
    it("Should return BadRequestException", async () => {
      expect(
        service.getBookByBookId({
          brand: "",
        }),
      ).rejects.toBeInstanceOf(BadRequestException);
    });

    it("Should return null", async () => {
      getBookByBookIdApi.execute.mockResolvedValue(null);

      const Books = await service.getBookByBookId(getBookQuery);

      expect(getBookByBookIdApi.execute).toHaveBeenCalledWith({
        bookId: getBookQuery.bookId,
      });
      expect(Books).toEqual(null);
    });

    it("Should return a Book", async () => {
      jest.spyOn(getBookByBookIdApi, "execute").mockResolvedValue(mockBook);

      const Book = await service.getBookByBookId(getBookQuery);

      expect(getBookByBookIdApi.execute).toHaveBeenCalledWith({
        bookId: getBookQuery.bookId,
      });
      expect(Book).toEqual(mockBook);
    });
  });
});

// GetBookByBookIdApi.ts
/* 
  in this file is only a http get request
*/
// ... the rest of the code
execute({bookId}: string) {
// ... the rest of the code
  return await this.httpService.request<Book[]>(config);
}
torpid depot
#

For someone who's new to testing it's not bad. Here are some things you can use for your current tests:

  • Your service really shouldn't throw a http exception, the idea behind a service is to keep http related things in a different boundary, however, if thats the only thing you are building then its up to you I suppose.
  • Remove the defined test. There is no point. If its not defined then it wont run.
  • Builder pattern for your entities. They add intent and clean up duplication (https://canro91.github.io/2021/04/26/CreateTestValuesWithBuilders/)
  • Utilize jest-mocked to create mocks
  • Try to follow some kind of convention like AAA(Arrange, Act, Assert) or whatever you prefer.
  • Your test descriptions aren't really explaining anything, but thats more personal preference I suppose. E.g. "Should return null" -> "When a book does not exist then it should return null" (this gets you close to Acceptance Tests which usually follow Given, When, Then but they should be used accordingly.

I am not a fan of mocking dependencies if I dont have to. It's quite common in the London TDD school because it goes outside-in but less common in the Chicago school because of inside-out. I would recommend to replace mocks with fakes because it adds less coupling to implementation details. Hell, if you can... push the logic down to your (domain)entities and utilize integration tests for your commands and queries instead. Unit tests that verify collaboration are always harder to refactor because there is always some kind of coupling to implementation. Therefore try to only use test doubles against things you don't control, like an email service. A database is something within your control, unless multiple services use it.

#

For example:

  test("When I query for a book that does not exist, Then it returns null", async () => {
    const book = TestBookBuilder.aSpidermanBook().build()
    const nonExistingBook = TestBookBuilder.aBook().build()
    const fakeApi = new FakeGetBookByBookIdApi([book]);
    const service = new BookService(fakeApi);

    const query = new GetBookQuery({ bookId: nonExistingBook.id })
    const result = await service.getBookByBookId(query)

    expect(result).toBe(null)
  })
#

Oh, you might like the Result pattern!

steady mortar
#

Hey, thanks a lot.
I know that there are many things to improve, I'll take your comment and for sure utilize some of the points.
What do you mean utilize jest-mocked to create mocks?
By AAA convention you mean to couple each "A" and to go in right order, arrange, act then assert?

torpid depot