Jest: How to test a component that dispatch an API action - jestjs

I am new to testing,
I am trying to understand if it's possible to test an API call that dispatch from the component.
Is it possible to wait for a response from the server?
import React from 'react';
import { render, fireEvent, cleanup, waitForElement } from 'react-testing-library';
import 'jest-dom/extend-expect'
import App from 'src/app';
import { store } from 'stories/index.stories.js';
import { Provider, connect } from 'react-redux';
const renderComponent = () => (<App />);
it('renders correctly', () => {
const { container, getByText, queryAllByText, getByPlaceholderText } = renderComponent();
expect(container).not.toBeEmpty();
expect(getByText(/Discover/i)).toBeTruthy();
const discoverBtn = getByText(/Discover/i);
fireEvent.click(discoverBtn); // this will dispatch an action from the component
//what should i do next ?
});

This is how I do it.
First I put all my fetch requests in a separate file, say /api/index.js. In this way I can easily mock them in my tests.
Then I perform the actions that a user would do. Finally I check that the API got called and that the DOM was updated correctly.
import { aFetchMethod } from './api'
// With jest.mock our API method does nothing
// we don't want to hit the server in our tests
jest.mock('./api')
it('renders correctly', async () => {
const { getByText } = render(<App />)
aFetchMethod.mockResolvedValueOnce('some data that makes sense for you')
fireEvent.click(getByText('Discover'))
expect(aFetchMethod).toHaveBeenCalledTimes(1)
expect(aFetchMethod).toHaveBeenCalledWith('whatever you call it with')
// Let's assume you now render the returned data
await wait(() => getByText('some data that makes sense for you'))
})

Related

Mocha, Supertest and Mongo Memory server, cannot setup hooks properly

I have a Nodejs, Express server that uses Mongodb as the Database. I am trying to write some tests but I cannot get it configured properly. I need to create a mongoose connection for each block ( .test.ts file) once and then clean up the db for the other tests. Depending on how I approach this I get two different behaviours. But first my setup.
user.controller.test.ts
import { suite, test, expect } from "../utils/index";
import expressLoader from "#/loaders/express";
import { Logger } from "winston";
import express from "express";
import request from "supertest";
import { UserAccountModel } from "#/models/UserAccount";
import setup from "../setup";
setup();
import { app } from "#/server";
let loggerMock: Logger;
describe("POST /api/user/account/", function () {
it("it should have status code 200 and create an user account", async function () {
//GIVEN
const userCreateRequest = {
email: "test#gmail.com",
userId: "testUserId",
};
//WHEN
await request(app)
.post("/api/user/account/")
.send(userCreateRequest)
.expect(200);
//SHOULD
const cnt: number = await UserAccountModel.count();
expect(cnt).to.equal(1);
});
});
And my other test post.repository.test.ts
import { suite, test, expect } from "../../utils/index";
import { PostModel } from "#/models/Posts/Post";
import PostRepository from "#/repositories/posts.repository";
import { PostType } from "#/interfaces/Posts";
import { Logger } from "winston";
#suite
class PostRepositoryTests {
private loggerMock: Logger;
private SUT: PostRepository = new PostRepository({});
#test async "Should create two posts"() {
//GIVEN
const given = {
id: "jobId123",
};
//WHEN
await this.SUT.CreatePost(given);
//SHOULD
const cnt: number = await PostModel.count();
expect(cnt).to.equal(1);
}
}
And my setup
setup.ts
import { MongoMemoryServer } from "mongodb-memory-server";
import mongoose from "mongoose";
export = () => {
let mongoServer: MongoMemoryServer;
before(function () {
console.log("Before");
return MongoMemoryServer.create().then(function (mServer) {
mongoServer = mServer;
const mongoUri = mongoServer.getUri();
return mongoose.connect(mongoUri);
});
});
after(function () {
console.log("After");
return mongoose.disconnect().then(function () {
return mongoServer.stop(true);
});
});
};
With the above setup I get
1) "before all" hook in "{root}":
MongooseError: Can't call `openUri()` on an active connection with different connection strings. Make sure you aren't calling `mongoose.connect()` multiple times. See: https://mongoosejs.com/docs/connections.html#multiple_connections
But if I don't import the setup.ts and I rename it to setup.test.ts, then it works but the data isn't cleared after each run so I actually have 10 new users created instead of 1. Every time I run it, it works and doesn't clear the data after it's finished.
Also I have a big issue where the tests hang and don't seem to finish. I am guessing that is because of the async, await in the tests or because of the hooks hanging.
What I want to happen is:
Each test should have it's own setup, the mongo memory server should be clean every time.
The tests should use async and await and not hang.
Somehow export the setup from 1) as a utility function so that I can reuse it in my code

Infinite loop when pushing into array using UseState()?

I would like to store the data of a response into an array for reuse. I am using Axios for this. The issue I receive is that when I push into the array, it loops getBoroughAndId() and keeps pushing into the array. I can tell because I get a console.log() response where it keeps telling me I am making too many requests. Any advice? Thanks.
Edit: After taking another gander, I think the issue is that the id is always changing when running getBoroughAndId. I'm not sure how to stop this.
import React, { useEffect, useState } from 'react';
import { airtableApi } from '../services/api/airtable';
import { BoroughDay, BoroughGroup } from '../types/api';
const IndexPage = () => {
const [boroughs, setBoroughs] = useState<BoroughDay[]>([]);
const [boroughGroups, setBoroughGroups] = useState<BoroughGroup[]>([]);
const getBoroughsAndDays = () => {
airtableApi
.getBoroughsAndDays()
.then((response) => {
setBoroughs(response);
})
.catch(() => { });
};
const getBoroughAndId = (id: string) => {
airtableApi
.getBoroughAndId(id)
.then((response) => {
console.log(response);
setBoroughGroups(arr => [...arr, response])
return response;
})
.catch(() => { });
}
useEffect(() => {
getBoroughsAndDays()
}, [boroughGroups])
return (
<>
{boroughs.map((data) => {
getBoroughAndId(data.id);
})}
</>
)
}
export default IndexPage
Here is my corrected code. It works a lot better now, with less nonsense and everything being done in the first function.
import React, { useEffect, useState } from 'react';
import { airtableApi } from '../services/api/airtable';
import { BoroughDay, BoroughGroup } from '../types/api';
const IndexPage = () => {
const [boroughs, setBoroughs] = useState<BoroughDay[]>([]);
const [boroughGroups, setBoroughGroups] = useState<BoroughGroup[]>([]);
const getBoroughsDays = () => {
airtableApi
.getBoroughsAndDays()
.then((response) => {
setBoroughs(response.records);
response.records.map((data) => {
setBoroughGroups(arr => [...arr, {id: data.id, "Borough": data.fields["Borough"]}])
})
})
.catch(() => { })
}
useEffect(() => {
getBoroughsDays();
}, [])
return (
<>
{boroughGroups.map(data => <div>{data.id} {data.Borough}</div>)}
</>
)
}
export default IndexPage
In order to tell you the mistake you are committing, I will tell you the whole flow of your program.
First of all when component mounts, useEffect will be called, which will call the getBoroughsAndDays function.
Note: boroughGroups in dependency array in useEffect is causing an infinite loop
This function (getBoroughsAndDays()) will update the value of boroughs(using setBoroughs)
Now since the state updated the function will re render, hence output will be shown on the screen
Now observe, here you are calling "getBoroughAndId(data.id)" function (inside map function), which is updating the value of boroughGroups(using setBoroughGroups)
Now since the value of boroughGroups have changed, the useEffect method will be called, which will again trigger the "getBoroughsandDays()" function, repeating the whole process again, so that is the reason, it is creating infinite loop.
Note: When any value inside dependency array changes useEffect will be called.
Solution:
I don't know what functionality you want to achieve but remove "boroughGroups" dependency from useEffect (In this way it will behave like componentDidMount).
You have a useEffect hook that updates state: boroughs whenever value of state: boroughGroups changes.
In the return statement, you iterate through boroughs and update boroughGroups.
Back to the first statement.
To stop this infinite loop, stop updating boroughGroups, that triggers useEffect everytime.
useEffect(() => {getBoroughsAndDays()}, []);

Jest not using manual mock

I'm using Create React App. I created a Jest test that uses a manual mock. The test renders my App component and I'm trying to mock a nested component. It's still picking up the original BarChart component.
containers/__tests__/App.js
import React from 'react';
import { shallow, mount } from 'enzyme';
const barChartPath = '../../components/d3/BarChart/BarChart';
describe('App', () => {
beforeEach(() => {
const mockBarChart = require('../../components/d3/BarChart/__mocks__/BarChart').default;
jest.mock(barChartPath, () => mockBarChart);
});
it('renders without crashing - deep', () => {
const App = require('../App').default;
mount(<App />);
});
});
I tried using
import BarChart from '../../components/d3/BarChart/BarChart';
...
beforeEach(() => {
jest.mock(BarChart)
but that didn't work either.
I'm using the require statement in the beforeEach due to the issue described in Manual mock not working in with Jest
setupTests.js
import { configure } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
configure({ adapter: new Adapter() });

React redux-saga on server side doesn't take action after browser reload

I have some problems with my universal react app runing with saga. I'm rendering react on server. One of my react component executes redux action that should be catched by saga listener on server.
Here is abstract example
// *Header.js*
class Header extends React.PureComponent {
componentWillMount() {
this.props.doAction()
}
....
}
export default connect(null, {doAction})(Header)
// *actions.js*
function doAction() {
return {
type: "action"
}
}
// *saga.js*
function* doAsyncAction(action) {
console.log(action);
}
function* watchAction() {
yield takeEvery("action", doAsyncAction);
}
export default [
watchAction(),
];
// *sagas.js* --> root saga
import 'regenerator-runtime/runtime';
import saga from './saga';
import anotherSaga from './anotherSaga'
export default function* rootSaga() {
yield all([].concat(saga).concat(anotherSaga));
}
// *configureStore.js*
const sagaMiddleware = createSagaMiddleware();
const middleware = applyMiddleware(sagaMiddleware);
...
sagaMiddleware.run(require('./sagas').default);
And after first run node process - it runs and give me console log, but
when I just refresh browser and function doAsyncAction is never executed
Please help, what I'm doing wrong ?
You need to change this:
function doAction() {
return {
type: "action"
}
}
to this:
const mapDispatchtoProps = (dispatch) => {
return {
doAction: () => dispatch({type: "action"})
}
}
export default connect(null, mapDispatchtoProps)(Header)
Client.js setup below for saga middleware:
const sagaMiddleware = createSagaMiddleware()
const createStoreWithMiddleware = applyMiddleware(sagaMiddleware)(createStore)
let store = createStoreWithMiddleware(rootReducers)
sagaMiddleware.run(rootSaga)
The above is implemented where ever you are implementing your store.

Unit testing only controller file instead of express framework

I am trying to unit test my controller file that handles a particular request. I do not want to test express as we have another mechanism for testing our Apis. I am stubbing the data using sinon. But the response is not what I am expecting.. I am always getting back a 200 status instead of the 201 that I am supposed to receive. Am I missing out on any specific part? I am using Typescript and mocha
my spec.ts file is
import {expect} from 'chai';
import {HiringCompanyJobHandler} from
"../../source/Handlers/HiringCompanyJobHandler";
import * as httpMocks from 'node-mocks-http';
import * as sinon from 'sinon';
import Jobs from "../../source/DAL/model/job";
import * as mongoose from 'mongoose';
import {Response, Request, Express} from 'express';
describe("Hiring Company Job Handler Unit Test Cases", () => {
let request = null;
let response = null;
beforeEach(function () {
response = httpMocks.createResponse();
});
it("Should return the job details that belong to a particular company", (done) => {
request = {
query: {
UserName: 'HiringCompany',
CompanyID: '1'
}
};
let expectedJobs = [
{
'hiringCompanyId': '1',
'companyJobId': 'T1',
'jobName': 'Plumber',
'jobType': 'Permanent'
}, {
'hiringCompanyId': '1',
'companyJobId': 'T2',
'jobName': 'Electrician',
'jobType': 'Temporary'
}
];
sinon.mock(Jobs)
.expects('find')
.resolves(expectedJobs);
let hiringCompanyJobHandler = new HiringCompanyJobHandler();
hiringCompanyJobHandler.GetBasicJobInformationForHiringCompany(request, response);
expect(response.statusCode).to.equal(201);
done();
})
});
My controller code is as follows
import { NextFunction, Request, Response } from "express";
import { isNullOrUndefined } from "util";
import JobDal from "../DAL/jobdal";
export class HiringCompanyJobHandler {
constructor() {
}
public GetBasicJobInformationForHiringCompany(req: Request, res: Response) {
let userName = req.query.UserName;
let companyID = req.query.CompanyID;
let status = req.query.status;
let jobDal = new JobDal();
jobDal.GetHiringCompanyJobDetails(companyID,status).then((details) => {
if (details.length > 0) {
res.status(201).send({ details });
}else {
res.status(200).send({ status: "No jobs found!" });
}
}).catch((error: any) => {
console.log(error);
res.status(500).send(error);
});
}
}
export default new HiringCompanyJobHandler();
resolves does not appear to be available for mocks (check the api here).
I guess that you want to modify the behavior of the method find in Job, and that this method is called inside GetHiringCompanyJobDetails. Is that correct?
I think though that you should stub the method GetHiringCompanyJobDetails instead, so your test is pure unit test, since in here you are unit testing the method GetBasicJobInformationForHiringCompany. But this would be part of a different discussion.
In any case, you should use a stub if you want to modify the behavior of a method to meet the requirements of the path you want to test.
Check the sinon API for stubs here. I hope this solves your issue. :)

Resources