Search code examples
reactjsecmascript-6jestjsenzymekentico

Jest Unit Test for chained function calls


Its been a while I am struggling with this. New to Unit testing.. I am trying to mock a unit test case for -

fetchFooter() {
    client.items().type('footer_link')
            .depthParameter(10)
            .toObservable()
            .subscribe((response) => {
                this.setState({
                    resFooter: response.items,
                    resFooterLinked: response.linkedItems,
                });
            });
}

Where client is

import { DeliveryClient } from '@kentico/kontent-delivery';

const client = new DeliveryClient({
  projectId: <projectKey>,
  enableAdvancedLogging: false,
});

This is what I have written till now, but doesn't seem to work.

const sample = {
  items: {},
  linkedItems: {}
};//this is my response object

describe("testing client", () => {
  const mockClient = {
    items: jest.fn().mockReturnThis(),
    type: jest.fn().mockReturnThis(),
    depthParameter: jest.fn().mockReturnThis(),
    toObservable: jest.fn().mockReturnThis(),
    subscribe: jest.fn().mockReturnThis(),
  };

  const mockProps = {
    client: mockClient,
  };
  const component = mount(
    <Footer mockProps={mockProps} resFooter={true} resFooterLinked={true} />
  );

  describe("Component", () => {
    describe("#componentDidMount", () => {
      it("should mount the component and set state correctly", () => {
        const mockedResponse = sample;
        mockClient.subscribe.mockImplementationOnce((handler) => handler(sample));
        // tslint:disable-next-line: no-string-literal
        component["setState"] = jest.fn();
        component.instance().fetchFooter();
        expect(
          mockClient.items().type().depthParameter().toObservable().subscribe
        ).toBeCalledWith("footer_link");
        // tslint:disable-next-line: no-string-literal
        expect(component["setState"]).toBeCalledWith(sample);
      });
    });
  });
});

I am getting an error as -

    expect(jest.fn()).toBeCalledWith(...expected)

    Expected: "footer_link"

    Number of calls: 0

Maybe I am missing some basics here. I searched for a solution for this, but unfortunately didn't find one which can work for me. Can anyone help with this? Thanks in advance


Solution

  • As the reference states, mockReturnThis is a shortcut for:

    jest.fn(function () {
      return this;
    });
    

    It only makes functions chainable and doesn't pass arguments to next function in a chain. subscribe isn't expected to be called with footer_link, it's type that is called with this argument.

    It's unnecessary to make mocked subscribe call a handler, this doesn't guarantee that setState has been called inside it. Also, setState is called not with sample but another object.

    It's unnecessary to call expect(mockClient.items()...) chain in a test, this just increases call count. Call assertions don't allow to determine the order in which methods have been called, this can be secured by hand.

    Since mocked client object should be accessible in config module mock, the variable needs to be lifted to module scope, this allows to mock client in component module.

    The chain can be fully tested like:

    // module scope
    jest.mock('../../config', () => ({
      __esModule: true,
      default: mockClient
    });
    
    const mockClient = {
      items: jest.fn(),
      type: jest.fn(),
      ...
    };
    ...
    
    // describe scope
    let clientCalls;
    beforeEach(() => {
      clientCalls = [];
    
      mockClient.items.mockImplementation(function () { clientCalls.push(1); return this });
      mockClient.type.mockImplementation(function () { clientCalls.push(2); return this });
      ...
    });
    ...
    
    // test scope
    component.instance().fetchFooter();
    
    expect(clientCalls).toEqual([1,2,3,4,5]);
    expect(mockClient.items).toBeCalledWith();
    expect(mockClient.type).toBeCalledWith("footer_link");
    ...
    expect(mockClient.subscribe).toBeCalledWith(expect.any(Function));
    
    let [handler] = mockClient.subscribe.mock.calls[0];
    expect(component["setState"]).not.toBeCalled();
    handler();
    expect(component["setState"]).toBeCalledWith({ resFooter:..., resFooterLinked:...});