2

I have an azure function like so (not the actual function, just a representation is given below)

import { AzureFunction, Context, HttpRequest } from '@azure/functions'
import { ServiceBusClient, isServiceBusError } from '@azure/service-bus'

const serviceBusClient = new ServiceBusClient(clientConnectionString)
const sender = serviceBusClient.createSender(topicName)

const func: AzureFunction = async function (
  context: Context,
  req: HttpRequest
): Promise<void> { 
try {
sender.sendMessages({ body: {a:"a",b:"b",c:"c" })
}
catch (error: unknown) {
if (isServiceBusError(error)) {
      message = `This is the error message ${error.message} with the reason ${error.code}`
      context.log.error(`Error Message: ${message}`)
    } else {
      context.log.error(`Error Message: Error Encountered`)
    }
}
}

How do to test if the code in the catchblock is working properly? Specifically, how do I throw a ServiceBusError to test that it is caught and appropriate error message is logged?

Can any one guide or provide hint on how to proceed?

Update

I already have the following code to mock the service bus & also to check is an error is a Service bus error. However, I am not able to understand how I can have this function to mock both a 'good' case (message sent successfully) and a 'bad' case (where ServiceBusError is thrown`).

jest.mock('@azure/service-bus', () => {
  return {
    isServiceBusError: jest.fn().mockImplementation((err: unknown) => {
      if (err === ServiceBusError) {
        return true
      } else return false
    }),
    ServiceBusClient: jest.fn().mockImplementation(() => {
      return {
        createSender: jest.fn().mockImplementation(() => {
          return {
            sendMessages: mockSendMessages,
          }
        }),
      }
    }),
  }
})

I test is like so

test('Bad case: Empty Data', async () => {
  const emptyData = {}

  const wrongRequest = {
    query: {},
    body: emptyData,
  }

  // Action
  await func(context, wrongRequest)

  expect(mockSendMessages).toHaveBeenCalledTimes(0)
})

How to use the mock to throw the error is where I am getting confused (fyi, the above test case works just fine).

moys
  • 7,747
  • 2
  • 11
  • 42

2 Answers2

3

Use mock testing

You need to replace the sender.sendMessages function with a mock version that you can control, e.g. throw an exception in your test case.

The technique of using mock versions of services, objects or functions that you can control in your test code to simulate arbitrary conditions, states or errors is a well established best practice. You can find a ton of information on the internet about mock testing.

General info about mocking:

Use Jest Manual Mocks to create a mock ServiceBusClient

In essence, you will have to use Jest manual mocks so that

  1. the ServiceBusClient import imports the mock version rather than the real one

  2. The mock version's createSender method returns a mock sender.

  3. The mock sender's sendMessages method does whatever you want it to in your test (e.g. throw a specific exception). You do that by configuring the mock before your test calls the code being tested.

Here are some Jest- and Typescript-specific resources that can help you:

Answer update for the question update

What you want to do is be able change how your mock ServiceBusClient behaves according to the needs of each test case. You're only showing snippets of your code, but it seems you only have one global mock with hardcoded behavior.

Notice how in the Jest Manual Mock examples, the mock fs has a __setMockFiles method. This allows each test case to set up different files for different tests.

Another way is to replace the mock using mockImplementation() or mockImplementationOnce()​:

You can replace all of the above mocks in order to change the implementation, for a single test or all tests, by calling mockImplementation() on the existing mock.

So you should be able to apply the example at the above link to your case:

import { ServiceBusClient } from '@azure/service-bus'


jest.mock('@azure/service-bus')


describe('When SoundPlayer throws an error', () => {
  
  // mock with behavior required for all tests withing
  // this describe block 
  beforeAll(() => {
    ServiceBusClient.mockImplementation(() => {
      return {
        createSender: jest.fn().mockImplementation(() => {
          throw //whatever err you want to test
        }),
      };
    });
  });

  it('Should throw an error when calling createSender', () => {
     // your test code here
  });
});

Inigo
  • 12,186
  • 5
  • 41
  • 70
  • Thanks for the answer. I already have the 'mock' implemented. I am just not able to figure our how I can control the `good` and `bad` case. Since I have to send multiple conditions & only one of them is a condition where `SeviceBusError` is thrown. I don't know how to control it. – moys Jan 16 '23 at 08:52
  • Let me know if my update for your update works for you. I've explained the gist of it, but you really need to read the Jest mock docs... they give you a lot of power. – Inigo Jan 18 '23 at 03:39
  • I have 4 test cases & in 3 of them I DO NOT WANT the error to be thrown. I want error to be thrown in only one of them. Having `sendMessages` throw an error will throw during all the test cases. I am struck at how to control that behavior. – moys Jan 18 '23 at 04:14
  • Did you not read my answer? "change how your mock ServiceBusClient behaves according to the needs of each test case." and "You can replace all of the above mocks in order to change the implementation, for a single test or all tests, by calling mockImplementation() on the existing mock." It's right there, and examples in the Jest docs. Good luck. – Inigo Jan 18 '23 at 10:00
1

Here's an example of a Jest unit test source matching your needs (I hope):

import type { AzureFunction, Context } from '@azure/functions'
import type { ServiceBusSender } from '@azure/service-bus'
import { when } from 'jest-when'

describe('service bus client unit tests', () => {
  const clientConnectionString = 'myConnectionString'
  const topicName = 'myTopic'
  let ServiceBusClient: jest.SpyInstance
  let isServiceBusError: jest.SpyInstance
  let serviceBusClient: { createSender: (topicName: string) => ServiceBusSender }
  let sender: jest.Mocked<ServiceBusSender>
  let func: AzureFunction
  beforeAll(() => {
    ServiceBusClient = jest.fn()
    isServiceBusError = jest.fn()
    serviceBusClient = { createSender: jest.fn() }
    sender = { sendMessages: jest.fn() } as unknown as jest.Mocked<ServiceBusSender>
    jest.doMock('@azure/service-bus', () => ({ ServiceBusClient, isServiceBusError }))
    when(ServiceBusClient).calledWith(clientConnectionString).mockReturnValue(serviceBusClient)
    when(serviceBusClient.createSender).calledWith(topicName).mockReturnValue(sender)
    func = require('./sample').func
    expect(ServiceBusClient).toHaveBeenNthCalledWith(1, clientConnectionString)
    expect(serviceBusClient.createSender).toHaveBeenNthCalledWith(1, topicName)
  })
  afterEach(() => {
    jest.resetAllMocks()
  })
  afterAll(() => {
    jest.restoreAllMocks()
  })
  test('should log error given a service bus error', async () => {
    // Given
    const contextLogError = jest.fn()
    const context = { log: { error: contextLogError } } as unknown as Context
    const messageBody = { body: { a: 'a', b: 'b', c: 'c' } }
    const error: Error & { code?: string } = new Error('oops')
    error.code = 'MyCode'
    when(sender.sendMessages).calledWith(messageBody).mockRejectedValue(error)
    when(isServiceBusError).calledWith(error).mockReturnValue(true)
    // When
    await func(context)
    // Then
    expect(sender.sendMessages).toHaveBeenNthCalledWith(1, messageBody)
    expect(isServiceBusError).toHaveBeenNthCalledWith(1, error)
    expect(contextLogError).toHaveBeenNthCalledWith(
      1,
      'Error Message: This is the error message oops with the reason MyCode',
    )
  })
  test('should log error given any other error', async () => {
    // Given
    const contextLogError = jest.fn()
    const context = { log: { error: contextLogError } } as unknown as Context
    const messageBody = { body: { a: 'a', b: 'b', c: 'c' } }
    const error = new Error('oops')
    when(sender.sendMessages).calledWith(messageBody).mockRejectedValue(error)
    when(isServiceBusError).calledWith(error).mockReturnValue(false)
    // When
    await func(context)
    // Then
    expect(sender.sendMessages).toHaveBeenNthCalledWith(1, messageBody)
    expect(isServiceBusError).toHaveBeenNthCalledWith(1, error)
    expect(contextLogError).toHaveBeenNthCalledWith(1, 'Error Message: Error Encountered')
  })
})