0

I have one object (ConvertkitServer) that contains two methods: addSubscriberTags() that calls getConvertkitTags(). Both use fetch() and the scope of this question is limited to the tests I’m creating for addSubscriberTags():

import fetch, { RequestInit, Response } from "node-fetch";
// .. omitted for brevity

async function getConvertkitTags(): Promise<ConvertkitTag[]> {
  console.log('REAL getConvertkitTags()');
  // .. omitted for brevity
  console.log('REAL tags count', convertkit_tags.length);
  return convertkit_tags;
}

async function addSubscriberTags(email: string, tags: string[]): Promise<void> {
  const convertkit_tags = await getConvertkitTags();
  // .. omitted for brevity

  console.log('addSubscriberTags().tags.length = ', convertkit_tags.length);

  const response: Response = await fetch(
    new URL(`${CONVERTKIT_API_URI}/tags/${tag_ids[0]}/subscribe`).toString(),
    {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        api_secret: process.env.CONVERTKIT_API_SECRET as string,
        email: email,
        tags: tag_ids
      })
    }
  );

  return;
}

export {
  addSubscriberTags,
  getConvertkitTags
}

In my test, I want to:

  1. force the return value of getConvertKitTags(); so I can
  2. check for specific arguments used in the call to fetch() in the addConvertkitTags() method.

Neither is working.

  • To handle (1), I’ve set up a spy to force a specific result. It should only return 2 items, but the console log statement from the mocked method is returning all 11. This tells me my spy isn’t working:

    import * as ConvertkitServer from './convertkit.server';
    
    test.only('it adds 1 tag without throwing an error', async() => {
      expect.assertions(1);
    
      // setup SUT
      jest.spyOn(ConvertkitServer, 'getConvertkitTags').mockResolvedValue([
        CONVERTKIT_TAGS_RESPONSE.tags[0],
        CONVERTKIT_TAGS_RESPONSE.tags[1],
      ]);
    
      // run SUT
      const response = await ConvertkitServer.addSubscriberTags('test@test.com', [CONVERTKIT_TAGS_RESPONSE.tags[0].name]);
    
      // check
      expect(response).toBeUndefined();
    });
    
      console.log
        REAL getConvertkitTags()
    
          at CONVERTKIT_API_SECRET (contacts/convertkit.server.ts:195:20)
    
      console.log
        REAL tags count 11
    
          at getConvertkitTags (contacts/convertkit.server.ts:2598:11)
    
      console.log
        addSubscriberTags().tags.length =  11
    
          at Object.addSubscriberTags (contacts/convertkit.server.ts:2447:11)
    

    I don't understand why my spy seems to be ignored as the underlying one is what's being called from what I see in the console.

  • Once that's fixed, I then need to test the arguments passed to fetch() within addSubscriberTags(), but after multiple attempts, I can't figure out how to properly spy on the fetch() method. I don't care if fetch() is called as I'm using MSW to intercept the calls. But I do want to check the arguments submitted, but all examples & questions/solutions I've found only address mocking fetch() which isn't what I want to do.

    I just want to spy on it and its arguments when called.

jonrsharpe
  • 115,751
  • 26
  • 228
  • 437
Andrew Connell
  • 4,939
  • 5
  • 30
  • 42
  • (**TL;DR**: [don't](https://stackoverflow.com/a/70066090/3001761). Given you're using [MSW](https://mswjs.io/) assert on the request, not the arguments to `fetch`.) – jonrsharpe Jul 22 '23 at 21:07
  • I don't understand your comment @jonrsharpe... I _do_ want to check the params in the `fetch`. The argument `tags` is an array of strings & the method gets a collection of tags to convert that array into an array of numbers (`tag_ids`). I want to ensure that based on the array passed in, it submits the correct array of numbers to the endpoint. There's more stuff going on that I removed to simplify the question. – Andrew Connell Jul 23 '23 at 10:37
  • The point is to, rather than `expect(fetch).toHaveBeenCalledWith(...)` (implementation), use the request object MSW exposes to assert on the path params, body, etc. (behaviour). Then you don't need to mock _any_ of what you're showing and can therefore freely and confidently refactor it. – jonrsharpe Jul 23 '23 at 11:04
  • Making sure I get this right... are you saying to test what `fetch` is sending, instead of testing fetch, test what MSW receives? So, effectively you ignore the headache of testing `fetch` and just look at what was received in the mocked response from MSW? If so... an interesting approach. I'd need to figure out how the test can determine what MSW sent back for that specific test run... – Andrew Connell Jul 23 '23 at 11:26
  • Actually, not sure I get how that works... because now I'm spying on the MSW, not what my core is actually calling. For instance, other methods (not shown in this question) result in multiple `fetch` calls and I'd like to have tests that check those. – Andrew Connell Jul 23 '23 at 11:30

1 Answers1

0

This was an interesting one. It has got to do with the way you are exporting your functions. When you import these functions into another file, you are actually importing from the export object, which means you are not directly working with the original functions but rather with the export objects references. As a result, with spyOn when you are targeting 'getConvertkitTags' function, you are actually spying on export.getConvertkitTags, not the original function, reference of which was enclosed with the function declaration. This causes the mock not to take effect as expected. My brain is getting all jumbled up and doing a poor job in explaining. here's an article that explains this topic beautifuly.
The only way I managed to make jest.spyOn to target the function properly was exporting them as arrow functions.

// export from functions file
export const fun1 = ()=> 42
export const fun2 =()=> fun1()

// import in test file 
import * as jsMockery from ...
jest.spyOn(jsMockery, 'fun1').mockResolvedValue(42) // this works
..... 

I hope we get some interesting suggestion from others.
p.s. from the comment i can see that there are other workarounds too. Still, seems a bit cumbersome to me tbh.

Nazrul Chowdhury
  • 1,483
  • 1
  • 5
  • 11