Jest + React component
Tags: jest, react, node, tdd
Published: 2023-07-20 12:16:54

## Test driven development

> If you are new to TDD, or jest, please have a look at the Resources section at the end of this post. 

### The code 

I have a React component, `SiteBreadcrumbs.tsx` that is using `BreadcrumbsHelper` class to generate site breadcrumbs. 

#### SiteBreadcrumbs.tsx
```tsx
import BreadcrumbsHelper from '../../../../utils/BreadcrumbsHelper';
import { BreadcrumbDataProvider } from '../../../../dal/BreadcrumbDataProvider';
import { PageContextDataProvider } from '../../../../dal/PageContextDataProvider';

React.useEffect(() => {
    //...
    const helper = new BreadcrumbsHelper(dataProvider, pageContextProvider);

    helper.getBreadcrumbs()
        .then((result) => {
             setLinkItems(result);
         })
         .catch((error) => {
             console.log(error);
          });
}, []);
```

#### BreadcrumbsHelper.ts
```ts
export default class BreadcrumbsHelper {

constructor(protected dataProvider: BreadcrumbDataProvider, protected pageContextProvider: PageContextDataProvider) {}

    public getBreadcrumbs = async (): Promise<IBreadcrumbItem[]> => {
        const items: IBreadcrumbItem[] = [];
        //...
        return items;
    }
    //...
}
```

### Tests

#### Not so great approach

Jest allows [mocking partials](https://jestjs.io/docs/mock-functions#mocking-partials), so I _could_ mock the `BreadcrumbsHelper.getBreadcrumbs()` method using the following code:
##### _SiteBreadcrumbs.test.tsx_
```ts
jest.mock('../../src/utils/BreadcrumbsHelper', () => {
    const originalModule = jest.requireActual('../../src/utils/BreadcrumbsHelper');
    return {
        __esModule: true, //<- required for ES modules
        ...originalModule,
        default: jest.fn().mockImplementation(() => {
            return {
                getBreadcrumbs: jest.fn().mockImplementation(() => {
                    return Promise.resolve([];
                })
            };
        }),
    };
});

```
But in my tests, I want to cover different cases: breadcrumbs for site's welcome page, site's (other) page, subsite, etc. And I certainly won't be creating separate files just to cover them. 

#### The right approach

_You will find many examples on how to mock functions, but the complicated part here is that `getBreadcrumbs` is a class method and is not exported as a function._

Jest requires the `jest.mock('../../src/utils/BreadcrumbsHelper')` to be above the tests, but I can change the `getBreadcrumbs` mock for each test using.

```ts
(BreadcrumbsHelper as jest.Mock).mockReturnValueOnce({
    getBreadcrumbs: jest.fn().mockResolvedValue(BreadcrumbsConfig.TeamSiteWelcomePage)
})
```
##### _SiteBreadcrumbs.test.tsx_
```tsx
import BreadcrumbsHelper from '../../src/utils/BreadcrumbsHelper';
jest.mock('../../src/utils/BreadcrumbsHelper')

const BreadcrumbsConfig = {
    TeamSiteWelcomePage: [
        { "text": "Intranet", "key": "1", "href": "https://contoso.sharepoint.com/sites/Intranet"}
    ],
    TeamSiteInsightsPage: [
        { "text": "Intranet", "key": "1", "href": "https://contoso.sharepoint.com/sites/Intranet"},
        { "text": "Insights", "key": "2", "href": ""}
    ],
    //...
}

describe('SiteBreadcrumbs', () => {
    it('should render breadcrumbs on TeamSite WelcomePage', async () => {
        (BreadcrumbsHelper as jest.Mock).mockReturnValueOnce({
            getBreadcrumbs: jest.fn().mockResolvedValue(BreadcrumbsConfig.TeamSiteWelcomePage)
        })
        await act(async () => {
            siteBreadcrumbs = render(<SiteBreadcrumbs pageContext={mockPageContext} spfiContext={mockSPFI} />);
        });
        //...
    });

    it('should render breadcrumbs on TeamSite InsightsPage', async () => {
        (BreadcrumbsHelper as jest.Mock).mockReturnValueOnce({
            getBreadcrumbs: jest.fn().mockResolvedValue(BreadcrumbsConfig.TeamSiteInsightsPage)
        })

        await act(async () => {
            siteBreadcrumbs = render(<SiteBreadcrumbs pageContext={mockPageContext} spfiContext={mockSPFI} />);
        });
        //...
    });
}
```

## Resources 
- [State of SPFx unit testing in 2023](https://mgwdevcom.wordpress.com/2023/05/29/state-of-spfx-unit-testing-in-2023/)
- [Testing Library](https://testing-library.com/)
- [Testing Library / Custom matchers](https://www.npmjs.com/package/@testing-library/jest-dom#custom-matchers)
- [Fluent UI / Testing with Jest](https://github.com/microsoft/fluentui/wiki/Testing-with-Jest)
- [Jest React Master](https://medium.com/crossplatform/jest-react-master-b4ac52a005d#19c5)
- [Configuring code coverage in Jest, the right way](https://www.valentinog.com/blog/jest-coverage/)
- [Full code-coverage with Jest](https://tsmx.net/jest-full-code-coverage/)
- [Azure DevOps - Run Unit tests](https://learn.microsoft.com/en-us/azure/devops/pipelines/ecosystems/customize-javascript?view=azure-devops#run-unit-tests)
- [Understanding Jest Mocks](https://medium.com/@rickhanlonii/understanding-jest-mocks-f0046c68e53c)
- [Change Jest mock per test](https://mikeborozdin.com/post/changing-jest-mocks-between-tests/)