Raystorm
Raystorm

Reputation: 6528

Mock AWS Amplify API and Storage for Unit Testing with V6

I have a Typescript/React Application leveraging Amplify to run in AWS. My Application leverages Amplify API (GraphQL) and Amplify Storage for working with Amazon Services.

in Amplify V5 I was able to Set Jest Functions for the relevant API calls so I could safely run unit tests.

setupTests.ts


import {API, Storage} from "aws-amplify";

/** Establish API mocking before all tests. */
beforeAll(() => {
  URL.revokeObjectURL = jest.fn();
  //Window.prototype.scroll = jest.fn();
  window.HTMLElement.prototype.scroll = jest.fn();
  window.HTMLDivElement.prototype.scroll = jest.fn();

  jest.mock('aws-amplify');
  API.graphql    = jest.fn();
  Storage.get    = jest.fn();
  Storage.copy   = jest.fn();
  Storage.remove = jest.fn();
});

I am now upgrading from V5 to V6 of amplify. The API changed. instead of import {API, Storage} from "aws-amplify"; and then running the functions on those objects, the functions are imported directly.

import {generateClient} from 'aws-amplify/api'
import {getUrl, downloadData, copy, remove, uploadData} from 'aws-amplify/storage';

I've tried a few different things to mock generateClient in setupTests.ts but nothing has worked.

jest.mock('aws-amplify');
jest.mock('aws-amplify/api');
jest.mock('aws-amplify/storage');
  jest.mock('aws-amplify/api',
            () => { return { generateClient: () => { graphql: jest.fn() } };
  });
jest.mock('generateClient', () => { graphql: jest.fn(); });
  jest.mock('aws-amplify/api', () => {
    return { generateClient: () => { graphql: jest.fn() } };
  });
import * as API from 'aws-amplify/api';
  
beforeAll(() => {  
  API.generateClient = jest.fn();
  const generateClient = jest.mocked(API).generateClient;
  const generateClient = API.generateClient;
});
jest.mocked(generateClient).mockImplementation(() => ({graphql: jest.fn()}));
  jest.mock('aws-amplify/auth');
  jest.mock('getUrl', () => jest.fn());
  jest.mock('downloadData', () => jest.fn());
  jest.mock('uploadData', () => jest.fn());
  jest.mock('copy', () => jest.fn());
  jest.mock('remove', () => jest.fn());
  // @ts-ignore
  generateClient.mockImplementation(() => ({graphql: jest.fn()}));

  // @ts-ignore
  getUrl.mockImplementation(() => {});
  // @ts-ignore
  downloadData.mockImplementation(() => {});
  // @ts-ignore
  uploadData.mockImplementation(() => {});
  // @ts-ignore
  copy.mockImplementation(() => {});
  // @ts-ignore
  remove.mockImplementation(() => {});

When testing with expect(client.graphql).not.toHaveBeenCalled(); where client = generateClient(); and was either later on in the same beforeAll method or a separate test file. I kept getting errors that client.graphQL is an actual function and not a mock, in the case of the *.mockImplementation() example the error was about that function not being available, because the code wasn't mocked yet.

TL/DR: In Amplify V6, with the direct method, and generator Exports, how do I setup my tests to just Jest Mock Functions in place of the real calls in my production code for unit testing?


Update:

if I put

jest.mock('aws-amplify/api', () => ({
   generateClient: jest.fn(() => { graphql: jest.fn() }),
}));

//@ts-ignore
when(generateClient).calledWith().mockReturnValue({ graphql: jest.fn() });

before describe( in my a test class, It successfully, mocks generateClient() and client.graphql(.
However, with V5, I was able to set those mocks in setupTests.ts and then they were available in any test files that needed them, without having to duplicate a lot of mocking code, thanks to beforeAll(.

The above code doesn't work in beforeAll( so How can I safely store and set the mocking code in one place, so I'm not duplicating a bunch of code to setup mocks everywhere?


Update 2 per request, minimal test code:

import {generateClient} from "aws-amplify/api";

jest.mock('aws-amplify/api');
const client = generateClient();

describe('App', () => {

  test('mocks Amplify correctly', () =>
  {
    console.log(`generateClient: ${generateClient}`);
    expect(jest.isMockFunction(generateClient)).toBeTruthy();
    expect(jest.isMockFunction(client)).toBeFalsy();

    console.log(`client: ${client}`);

    expect(client).toHaveProperty('graphql');
    expect(jest.isMockFunction(client.graphql)).toBeTruthy()
    expect(client.graphql).not.toHaveBeenCalled();
  });
});

That's what I've been using to verify I built expected Mock object correctly.
I have some helper files and other tests that use when() to tell the mock objects to returned canned test data.

like so:

test('Documents still display when getDocuments returns an error.',
       async () =>
{
   //setup mocking, with forced error
   when(client.graphql)
     .calledWith(expect.objectContaining({query: queries.listDocumentDetails} ))
     .mockRejectedValue(errorDocList);
 
   const { store } = renderPage(DASHBOARD_PATH, <Dashboard />, state);

   //check page renders
   expect(screen.getByText(RecentDocumentsTitle)).toBeInTheDocument();

   //check for error message
   const msg = `Failed to GET DocumentList: ${errorDocList.errors[0].message}`;
   const errorMsg = buildErrorAlert(msg);
   await waitFor(() => {
     expect(store.getState().alertMessage).toEqual(errorMsg);
   });

   const doc = errorDocList.data.listDocumentDetails.items[1]!;

   expect(screen.getByText(doc.eng_title)).toBeInTheDocument();
   expect(screen.getByText(doc.bc_title)).toBeInTheDocument();
   expect(screen.getByText(doc.ak_title)).toBeInTheDocument();
};

Upvotes: 1

Views: 743

Answers (1)

Raystorm
Raystorm

Reputation: 6528

I figured out my problem.

The solution was Manual Mocks. The Documentation says to create a __mocks__ folder next to node_modules when mocking code from a module. That didn't work for my project. The solution was to put the __mocks__ folder at the root of the src directory for my project.

Upvotes: 1

Related Questions