Express 테스트 코드에서의 DB 커넥션 문제! (Jest has detected the following 1 open handle potentially keeping Jest from exiting: TCPWRAP)

maketheworldwise·2022년 11월 3일
0


이 글의 목적?

최근에 가고싶은 회사의 공고가 올라왔다. 문제는 나는 Java/Spring 개발자인데 회사에서 원하는 인재는 NodeJS를 능숙하게 다룰 줄 아는 인원이었다.

처음에 나도 "언어가 다르니까 포기해야하나?"라는 생각을 했지만 개발자라면 어떤 언어든 환경이든 변화에 유연하게 대처할 줄 알아야한다는 점을 떠올렸다. 그리고 이대로 포기하기에 너무 아깝다는 생각이 들어서 도전해보고자 벼락치기로 Express를 공부하기 시작했다.

공부를 하면서 "그래도 테스트 코드는 한번이라도 작성해봐야하지 않을까?"싶었다. 그래서 간단하게 만든 CRUD API에 대한 테스트 코드를 만들고 돌려보았는데 콘솔창에 이상한 메세지와 함께 테스트 코드가 끝나지 않는 문제가 발생했다. (한번에 통과하지 못할거라는 예상은 했어서 크게 멘붕이 오지는 않았다. 키득 🤭)

 PASS  test/post.test.ts
  Post API
    ✓ [POST] http://localhost:8080/post (37 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        3.389 s, estimated 4 s
Ran all test suites.
Jest did not exit one second after the test run has completed.

This usually means that there are asynchronous operations that weren't stopped in your tests. Consider running Jest with `--detectOpenHandles` to troubleshoot this issue.

어떤 문제일까? 🫠

문제점

테스트 옵션 추가

우선 내가 찾은 레퍼런스들에 따르면 테스트를 돌릴 때 옵션을 지정해주는 것이었다. (처음 확인한 오류에서도 --detectOpenHandles 옵션으로 트러블 슈팅하라고도 했으니까 😉)

// package.json
{
  "scripts": {
    "test": "DOTENV_CONFIG_PATH=.env.test jest --setupFiles=dotenv/config --runInBand --detectOpenHandles --forceExit",
    "start": "node dist/server.js",
    "build": "tsc -p ."
  },
}

--forceExit 덕분인지 테스트 코드가 끝나지 않은 문제는 해결된것 같다. 하지만 다른 메세지가 떠올랐다.

Jest has detected the following 1 open handle potentially keeping Jest from exiting:

  ●  TCPWRAP

TCPWRAP 문제

처음 TCPWRAP을 보고 가장 먼저 떠올린것은 TCPWrapper(호스트 기반 네트워킹 ACL 시스템)였다. 하지만 내가 별도로 MySQL 서버에 TCPWrapper 설정을 안했기 때문에 TCPWrapper와 관련없는 내용이라 판단했다.

그래서 이제 구글 선생님의 힘을 빌리고자 검색을 하기 시작했다.

검색해봤더니 Jest has detected the following 1 open handle potentially keeping Jest from exiting:에 대한 레퍼런스들은 찾을 수 있었지만 TCPWRAP에 관련된 내용은 찾기 힘들었다. 내가 찾은 레퍼런스들은 거의 공통적으로 앞에서 이미 구성한 옵션들을 지정해주라는 내용들이었다.

일단 DB와 관련된 내용일꺼라 80%(?) 확신해서 테스트 코드들을 하나하나 주석처리해보면서 확인해봤다. beforeAll(), afterAll() 픽스처 위주로 확인해봤는데 afterAll()에서 DB 커넥션을 끊어주는 부분에서 힌트를 얻을 수 있었다.

  afterAll(async () => {
    await MysqlDataSource.query(`TRUNCATE POST`);
    await MysqlDataSource.destroy();
  });

destroy() 코드를 주석처리를 해보니 한번만 나왔던 문구가 두번 반복되어 콘솔창에 출력되는 것을 확인할 수 있었다.

이를 통해 "아 이건 DB 연결이 계속 살아있어서 생기는 문제구나!"라는 것을 깨달았다. 즉, 테스트 코드에서 DB를 연결하고 끊어주는 것은 잘 수행되고있지만 프로젝트 내에서 DB 연결이 계속 이루어지고 있어서 발생한 문제라고 생각했다.

이전에 얼핏 들었던 내용으로는 서버를 구동할 때 전체 파일을 읽는다고 들었던것 같다. 그리고 내 DB 커넥션 파일을 확인하면 initialize()가 적혀있는데, 이 부분이 테스트를 돌릴 때도 같이 실행이 되면서 연결이 이루어졌다고 생각했다.

import { DataSource } from 'typeorm';
import 'dotenv/config';

const MysqlDataSource = new DataSource({
  type: 'mysql',
  host: process.env.DB_HOST,
  port: 3306,
  username: process.env.DB_USERNAME,
  password: process.env.DB_PASSWORD,
  database: process.env.DB_DATABASE,
});

MysqlDataSource.initialize()
  .then(() => {
    console.log('Data Source has been initialized.');
  })
  .catch(err => {
    console.error('Data Source initiate failed: ', err);
  });

export { MysqlDataSource };

결론적으로 이유는 확실하다고 할 수는 없지만 - 테스트 코드를 돌릴 때 파일을 읽는 과정에서 한번, 테스트 코드의 beforeAll() 픽스처에서도 한번 - 두 번 연결이 되는데 테스트 코드에서 afterAll()에서 DB 연결을 한번 끊어줬기에 단 하나의 TCPWRAP 오류가 난걸로 생각했다.

해결 방안

임시 방편으로 테스트 코드가 아무런 메시지 없이 통과시키기 위해서 DB 커넥션 설정 파일의 initialize() 코드를 주석처리해주었다. 그 결과 아무런 문구 없이 깨끗하게 통과가 되는 것을 확인할 수 있었다.

또 다른 해결법이 있을지 찾아봐야겠다. 🥹


2022.11.04 기록

다시 생각해보니 전체 파일을 읽는것이 아닌 것 같다. 즉, 전체 파일을 읽는게 아니라 nodemon server.ts 명령어를 입력했을 때 해당 server.ts 파일에 import되어있는 파일들을 읽는 것 같다. 따라서 확실하지는 않지만 initialize() 코드는 DB 커넥션 설정 파일을 불러올때 실행되는 것 같다.

내가 테스트를 통과하고 서버를 구동시켰을 때 아무런 문제 없이 돌아가게 하기 위해 구성한 방법은 server.ts 파일에 initialize() 코드를 옮겨넣는 것이었다. (DB 커넥션 파일에서는 코드를 삭제!)

import http from 'http';
import 'dotenv/config';

import createApp from './app';
import { MysqlDataSource } from './configs/db.config';
import { Request, Response } from 'express';

MysqlDataSource.initialize()
  .then(() => {
    console.log('Data Source has been initialized.');
  })
  .catch(err => {
    console.error('Data Source initiate failed: ', err);
  });

const app = createApp();
app.get('/ping', (_: Request, res: Response) => {
  res.status(200).json({ message: 'pong' });
});

/**
 * [POST] http://localhost:8080/post/test
 *
 * @author kevin
 * @description Post test
 *
 * @param title title
 * @param descriptions descriptions
 */
app.post('/post/test', async (req, res) => {
  const { title, descriptions } = req.body;

  await MysqlDataSource.query(
    `INSERT INTO POST(
      title,
      descriptions
    ) VALUE (?, ?);`,
    [title, descriptions]
  );
  res.status(201).json({ message: 'post test created' });
});

const server = http.createServer(app);
const serverPort = process.env.SERVER_PORT;

server.listen(serverPort, () => {
  console.log('Server is running on port:', serverPort);
});

결론적으로 기존에 내가 작성한 테스트 코드에서 DB 커넥션 설정 파일을 불러오고, 그에 따라 해당 파일에 작성된 initialize() 코드가 실행되는 문제로 보고있다.

이 글의 레퍼런스

profile
세상을 현명하게 이끌어갈 나의 성장 일기 📓

0개의 댓글