1

I have the following simple sample React app the frontend of which runs on port 5000 and the backend Express server runs on port 5001. The relationship between the two is established via "proxy": "http://localhost:5001", in package.json.

On the frontend, if "Submit" is clicked, it sends a POST request to /sample route of the backend and prints the status (200) to the screen. This all works fine.

I would now like to test this behavior with react-testing-library (or any other similar package). I have set up the following test file; however, when it runs, I get Error: connect ECONNREFUSED 127.0.0.1:80. It is almost if the testing library is not looking in the correct place for the backend? Is there a way to specify this in a config?

I saw some other posts with this error, they advise to include a leading "/" at front of the URL, but I already have this in my case (i.e. /sample/). Jest test passed but get Error: connect ECONNREFUSED 127.0.0.1:80 at the end

Frontend

function App() {
  const [responseStatus, setResponseStatus] = useState<number>();
  const { handleSubmit, register } = useForm();

  const onSubmit = (data: any) => {
    console.log(data);
    fetch('/sample/', {
      method: 'POST',
      body: data,
    }).then((resp) => setResponseStatus(resp.status));
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <p>Welcome</p>
      <p>{responseStatus}</p>
      <input {...register('fieldNo1')} />
      <button type="submit" data-testid="submit-button">
        Submit
      </button>
    </form>
  );
}

Express backend

var router = express.Router();

router.post('/', function (req, res, next) {
  res.sendStatus(200);
});

Test file

test('sample test', async () => {
  render(<App />);

  const welcomeText = screen.getByText('Welcome');
  expect(welcomeText).toBeInTheDocument();

  const submitButton = screen.getByTestId('submit-button');
  fireEvent.click(submitButton);

  const responseText = await screen.findByText('200');
  expect(responseText).toBeInTheDocument();
});

package.json

{
  "name": "sample",
  "version": "0.1.0",
  "private": true,
  "proxy": "http://localhost:5001",
  "dependencies": {
    "@testing-library/jest-dom": "^5.16.4",
    "@testing-library/react": "^13.1.1",
    "@testing-library/user-event": "^13.5.0",
    "@types/jest": "^27.4.1",
    "@types/node": "^16.11.27",
    "@types/react": "^18.0.6",
    "@types/react-dom": "^18.0.2",
    "react": "^18.0.0",
    "react-dom": "^18.0.0",
    "react-hook-form": "^7.33.1",
    "react-scripts": "5.0.1",
    "supertest": "^6.2.3",
    "typescript": "^4.6.3",
    "web-vitals": "^2.1.4"
  },
  "scripts": {
    "start": "set PORT=5000 && react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
  },
  "eslintConfig": {
    "extends": [
      "react-app",
      "react-app/jest"
    ]
  },
  "browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  },
  "devDependencies": {
    "@types/supertest": "^2.0.12"
  }
}
savreline
  • 131
  • 1
  • 7
  • The proxy is not involved in those tests. That's part of the Webpack dev server, which isn't running at all. – jonrsharpe Jul 11 '22 at 22:09
  • I understand that proxy isn't involved in those tests; however, how would I test components that make fetch calls to the backend without getting a connection error. – savreline Jul 11 '22 at 22:16
  • Extract the calls from the component to a service you can replace with a test double? Use e.g. msw to mock at the transport layer? If you know the proxy's not involved then note it's irrelevant to the question. – jonrsharpe Jul 11 '22 at 22:27
  • 1
    Thank you! Reason why I asked talked about proxy is that I was hoping there would be a similar dev package to configure within react testing library that would direct the API calls to the backend to achieve full e2e testing without mocks. – savreline Jul 13 '22 at 18:25

1 Answers1

1

I had a similar requirement to you; my solution was pretty much as @jonrsharpe suggested in the comments - I used msw to proxy through to my APIs (either local or real)

I took this a step further and created both a proxy handler through msw which both proxies and saves the responses to file, and then a mock handler which basically just replays the recorded responses when needed.

I tend to swap between them to:

  • Update the mock payloads when required (and\or test against the real APIs)
  • Use the mock versions when speed and repeatability are required (e.g. in CI pipelines)

This is my typescript code:

import { rest, setupServer } from 'msw';
import { headersToObject } from 'headers-utils';
import fs from 'fs/promises';

// This proxies everthing to real APIs - useful to update mock-responses
export const proxyAllHandlers = [
    rest.all('/*', async (req, res, ctx) => {
        const proxyUrl = new URL(req.url.pathname, 'https://<my-real-url>')
        const proxy = await ctx.fetch(proxyUrl.href, {
            headers: { authorization: "<auth token if required>" }
        });

        let text = await proxy.text();
        console.log(`${req.method} ${req.url.pathname}`, text);
        try {
            // also save to file
            let filename = `./src/tests/mock-responses/${req.method.toUpperCase()}${req.url.pathname.replaceAll("/", "-")}.json`;
            console.log(`Saving ${filename}...`);
            await fs.writeFile(filename, JSON.stringify(JSON.parse(text), null, '\t'), 'utf-8');
        }
        catch(err) {
            console.error("Error saving!", err);
        }

        return res(
            ctx.status(proxy.status),
            ctx.set(headersToObject(proxy.headers)),
            ctx.body(text)
        )
    }),
];

// This mocks all responses from the apis by replaying previous recorded payloads
export const mockHandlers = [
    rest.all("/*", async (req, res, ctx) => {
        let filename = `./src/tests/mock-responses/${req.method.toUpperCase()}${req.url.pathname.replaceAll("/", "-")}.json`;
        let data = await fs.readFile(filename, 'utf-8');
        return res(ctx.json(JSON.parse(data)));
    })
]

// Use proxyAllHandlers to record all expected requests as mocks and use mockHandlers to mock them
export const server = setupServer(...proxyAllHandlers);
beforeAll(() => {
  // Establish requests interception layer before all tests.
  server.listen()
});

afterAll(() => {
  // Clean up after all tests are done, preventing this
  // interception layer from affecting irrelevant tests.
  server.close()
});

Credit to this github thread which had the proxy code using msw: https://github.com/mswjs/msw/discussions/887

simonfanz
  • 31
  • 4