Wecode - Project 3 (부트캠프)/Project 3 과정

Project 3: unit test, test code [개념] + 프로세스

JBS 12 2023. 10. 20. 20:47
테스트 코드를 계획 및 작성하는 이유: 사전에 에러를 방지하여 더 높은 품질의 소프트웨어를 제공하기 위함

기능 추가, 버그 수정, 리팩토링을 진행하면서 개발자는 다양한 실수를 할 수 있음

개발자는 직접 의도한 대로 동작하는지 테스트하는 코드를 작성하여
사전에 미리 실수를 잡아내고 코드를 올바르게 수정


테스트를 통해 소프트웨어 개발 전반적인 부분에서 발생할 수 있는 에러를 미리 찾아낸다는 것은
테스트 환경이 실제로 서비스가 배포되는 환경과 일치해야 한다는 것

 

 

0. 프로젝트 구조

app.js ,server.js 분리하기 
서버 가동에 필요한 모든 코드가 app.js에 포함된 방식이 아닌, 
app.js와 server.js로 분리하는 방식으로 변경

 

app.js
const express = require("express");
const cors = require("cors");
const morgan = require("morgan");

const { routes } = require("./src/routes");

const createApp = () => {
  const app = express();

  app.use(express.json());
  app.use(cors());
  app.use(morgan("combined"));

  app.use(routes);

  return app;
};

module.exports = { createApp };
  • 애플리케이션의 router, middleware 관련 코드만 남겨두고 app을 리턴하는 createApp 함수를 생성합니다.

const createApp = () => {
  const app = express();

  app.use(express.json());
  app.use(cors());
  app.use(morgan("combined"));

  app.use(routes);

  return app;
};

module.exports = { createApp };

  • 테스트 단계에서는 테스트용 request를 활용하기 때문에 따로 listen할 필요 없이 해당 createApp 함수만 활용

 

server.js 
require("dotenv").config();

const { createApp } = require("./app");
const { AppDataSource } = require("./src/models/data-source");

const startServer = async () => {
  const app = createApp();
  const PORT = process.env.PORT;

	await AppDataSource.initialize();

  app.listen(PORT, () => {
    console.log(`Listening on Port ${PORT}`);
  });
};

startServer();
  • app은 app.js에서 만들어 놓은 createApp 함수를 활용합니다.

const { createApp } = require('./app');
const { AppDataSource } = require('./src/models/dataSource');

 

 

  • AppDataSource.initialize() , app.listen() 을 담고 있는 startServer 함수를 실행합니다.

const startServer = async () => {
  const app = createApp();
  const PORT = process.env.PORT;

  await AppDataSource.initialize();
  app.listen(PORT, () => {
    console.log(`Listening on Port ${PORT}`);
  });
};

startServer();

 

따라서 서버를 켤 때에는 server.js를 실행하고, 테스트 코드에서는 createApp 함수의 리턴 값인 app만 활용합니다.

 


1. test 폴더에 order.test로 만듦 

 


2.  test용 데이터베에스 만들기 _ connection_test 

 

create database [ ] 

 



3. 테스트용 dotenv 설정

테스트를 진행할 때 실제 프로덕션 환경의 데이터베이스를 활용하면 문제가 발생하기 때문에
테스트 전용 환경 변수를 따로 관리

jest를 이용하게 되면
테스트를 실행할 때 다른 환경 변수 파일을 불러와서 DB 커넥션에 활용할 수
따라서 테스트용 dotenv에는 테스트 환경에 맞는 값을 추가해줘야 합니다

 

 

1) .env.test  , .env.tests.sample 파일 만들고,

2) 데이터베이스 이름 넣기 ->  무조건 connection test database

.env.test에 password 와 database url 자리에 password를 넣어놓고, 

.gitignore에는 .env.test 를 넣어둬야 한다. 
그래야 원격 저장소에 안 올라가지 

 

3) .gitignore에 .env.test 쓰고, 

 

그 과정에서 생기는 오류는 아래 링크 참고 

 

git cache [.gitignore에 있지만 repository에 올라간 파일 없애기]

이후에 파일 추가를 하는데 repository에 올라가면 안 되는 경우, 1. .gitignore에 추가를 한다 그냥 텍스트로 typing하면 된다 2. 그런데 이미 그 전에 git add. git commit을 해서 repository에 올라가 있다. git ca

pm-developer-justdoit.tistory.com

 


3. package.json scripts에 명령어를 추가 (jest를 활용하여 테스트를 진행할 때 해당 파일을 config로 테스트할 수 있도록)

"scripts": {
    "test": "DOTENV_CONFIG_PATH=.env.test jest --setupFiles=dotenv/config"
  },

Server 작동 시에,

 

package.json을 보면, scripts 에

test , start이 있다.

 

 

원래는 nodemon app.js 를 하면, 서버 작동이 됐는데,

저거를 깔고 나서는, npm start 를 하면 된다.

nodemon을 할 거였으면, nodemon server.js 하면 된다.

 

  • test code 실행을 위해서는,  npm test  --> 에러가 안 날 때까지 하는 거 
  • 서버 작동은, Npm start

 


4. message에는 postman에 찍히는 res 값 “message": "Created Orders 주문정보 저장”

 

 

 

/ordes API는 하나여도, 

단계별 하는 동작이 다르니 

 


5. 이후 jest를 다운 받아야 한다. 

나의 경우는 아래와 같이 되어 있다

 


6. unit test 작성

그렇다면, 테스트 코드에는 어떤 게 들어가느냐 

// tests/user.test.js

// npm i --save-dev supertest
const request = require("supertest");

// supertest의 request에 app을 담아 활용하기 위해 createApp 함수를 불러옵니다.
const { createApp } = require("../app");
// DB와의 커넥션을 위해 DataSource 객체를 불러옵니다.
const { myDataSource } = require("../src/models/data-source");

describe("Sign Up", () => {
  let app;

  beforeAll(async () => {
    // 모든 테스트가 시작하기 전(beforeAll)에 app을 만들고, DataSource를 이니셜라이징 합니다.
    app = createApp();
    await AppDataSource.initialize();
  });

  afterAll(async () => {
    // 테스트 데이터베이스의 불필요한 데이터를 전부 지워줍니다.
    await AppDataSource.query(`TRUNCATE users`);

    // 모든 테스트가 끝나게 되면(afterAll) DB 커넥션을 끊어줍니다.
    await AppDataSource.destroy();
  });

  test("FAILED: invalid email", async () => {
    // supertest의 request를 활용하여 app에 테스트용 request를 보냅니다.
    await request(app)
      .post("/users/signup") // HTTP Method, 엔드포인트 주소를 작성합니다.
      .send({ email: "wrongEmail", password: "password001@" }) // body를 작성합니다.
      .expect(400) // expect()로 예상되는 statusCode, response를 넣어 테스트할 수 있습니다.
      .expect({ message: "invalid email!" });
  });

  // 다음과 같이 본인이 작성한 코드에 맞춰 다양한 케이스를 모두 테스트해야 합니다.
  // 그래야 의도에 맞게 코드가 잘 작성되었는지 테스트 단계에서부터 확인할 수 있습니다!
  test("SUCCESS: created user", async () => {
    await request(app)
      .post("/users/signup")
      .send({ email: "wecode001@gmail.com", password: "password001@" })
      .expect(201);
  });

  test("FAILED: duplicated email", async () => {
    await request(app)
      .post("/users/signup")
      .send({ email: "wecode001@gmail.com", password: "password001@" })
      .expect(409)
      .expect({ message: "duplicated email" });
  });
});

 

 

[추후 복사용 코드 재작성]   tests 폴더의 user.test.js 파일


// npm i --save-dev supertest
const request = require("supertest");

// supertest의 request에 app을 담아 활용하기 위해 createApp 함수를 불러옵니다.
const { createApp } = require("../app");
// DB와의 커넥션을 위해 DataSource 객체를 불러옵니다.
const { myDataSource } = require("../src/models/data-source");

describe("Sign Up", () => {
  let app;

  beforeAll(async () => {
    // 모든 테스트가 시작하기 전(beforeAll)에 app을 만들고, DataSource를 이니셜라이징 합니다.
    app = createApp();
    await AppDataSource.initialize();
  });

  afterAll(async () => {
    // 테스트 데이터베이스의 불필요한 데이터를 전부 지워줍니다.
    await AppDataSource.query(`TRUNCATE users`);

    // 모든 테스트가 끝나게 되면(afterAll) DB 커넥션을 끊어줍니다.
    await AppDataSource.destroy();
  });

  test("FAILED: invalid email", async () => {
    // supertest의 request를 활용하여 app에 테스트용 request를 보냅니다.
    await request(app)
      .post("/users/signup") // HTTP Method, 엔드포인트 주소를 작성합니다.
      .send({ email: "wrongEmail", password: "password001@" }) // body를 작성합니다.
      .expect(400) // expect()로 예상되는 statusCode, response를 넣어 테스트할 수 있습니다.
      .expect({ message: "invalid email!" });
  });

  // 다음과 같이 본인이 작성한 코드에 맞춰 다양한 케이스를 모두 테스트해야 합니다.
  // 그래야 의도에 맞게 코드가 잘 작성되었는지 테스트 단계에서부터 확인할 수 있습니다!
  test("SUCCESS: created user", async () => {
    await request(app)
      .post("/users/signup")
      .send({ email: "wecode001@gmail.com", password: "password001@" })
      .expect(201);
  });

  test("FAILED: duplicated email", async () => {
    await request(app)
      .post("/users/signup")
      .send({ email: "wecode001@gmail.com", password: "password001@" })
      .expect(409)
      .expect({ message: "duplicated email" });
  });
});

 

 

8.  npm test 명령어를 입력하여 실행

 

 

9. 아래와 같은 에러가 pass로 나올 때까지 수정 

 

pass, 성공한 거 보고 싶으면 아래 링크 

 

Project 3 -[주문 api] test code 성공

test code는 코드를 수정할 때마다 test code 수정되어야 하고 에러 수정을 해야 해서 가장 마자막 단계에서 하는 게 좋다 1. 에러 팁 [수정] 2. 에러 팁 [수정] 3. 성공! [full test code]

pm-developer-justdoit.tistory.com

 

 


[에러 해결한 전체코드] 

 

const request = require('supertest');                           // 실행은 npm test() --> package.json 활용

 


// supertest의 request에 app을 담아 활용하기 위해 createApp 함수를 불러옵니다.


const { createApp } = require('../app');                         //server.js에 있는 거

 

 


// DB와의 커넥션을 위해 DataSource 객체를 불러옵니다.


const { AppDataSource } = require('../src/models/dataSource');                       // 활용되는 database 말고, test로 만들어서

 


// 동작을 describe


describe('make order and pay', () => {
  let app;

 


  beforeAll(async () => {
    // 모든 테스트가 시작하기 전(beforeAll)에 app을 만들고, DataSource를 이니셜라이징 합니다.
    app = createApp();
    await AppDataSource.initialize();

 

 


    // 각 코드 순서 중요 -> data 들어가는 flow 에 맞춰서 

    // postman에 실제로 req.body에 넣고 통신하기 위해  mysql 테이블에 넣었던 데이터들
    // 적을 때는, 하드 코딩해서 직접 값을 key에 입력해주는 것


    // users 테이블 채우기
await AppDataSource.query(`
       INSERT INTO users (name, email, password, phone_number, zip_code, address, address_details)  VALUES   ('ddd', 'jdh@naver.com', '111', '010', '00000', '강남구', '101동')  
    `);

    // sellers 채우기
await AppDataSource.query(`
       INSERT INTO sellers (name, image, zip_code, address, address_details, phone_number) VALUES ('ddd', 'url', '111', '강남구', '101동 ', '010')  
    `);
    // products 테이블 채우기
await AppDataSource.query(`
    INSERT INTO product_categories (category_name) VALUES ('뷰티') 
  `);
    // products 테이블 채우기
await AppDataSource.query(`
    INSERT INTO products (id, name, images, price, product_category_id, seller_id) VALUES ( '1', '감자',"url",'1000', 1, 1) 
    `);
    // carts 채우기
await AppDataSource.query(`
    INSERT INTO carts (user_id, product_id, quantity) VALUES (1,1,3) 
    `);
    // paymemts 채우기

await AppDataSource.query(`
    INSERT INTO payments (method) VALUES ("s") 
    `);
  });

 

 


  afterAll(async () => {
    // truncate: 특정 테이블 비움; 테스트 데이터베이스의 불필요한 데이터를 전부 지워줍니다. -> 그래서 .env 에 test database 넣어야 함
    // 위에 table들 여기에 넣음 _ 근데 flow 순서대로 userId > productId > carts> orderId 등
    await AppDataSource.query(`SET foreign_key_checks = 0;`); //외래키 비활성화
    await AppDataSource.query(`TRUNCATE users`);
    await AppDataSource.query(`TRUNCATE sellers`);
    await AppDataSource.query(`TRUNCATE product_categories`);
    await AppDataSource.query(`TRUNCATE products`);
    await AppDataSource.query(`TRUNCATE carts`);
    await AppDataSource.query(`TRUNCATE payments`);
    await AppDataSource.query(`SET foreign_key_checks = 1;`); // 외래키 다시 활성화

 

 


    // 모든 테스트가 끝나게 되면(afterAll) DB 커넥션을 끊어줍니다.
    await AppDataSource.destroy();
  });

  test('SUCCESS: created orders ', async () => {
    const res = await request(app)
      .post('/orders')
      .send({
        userId: 1,
        totalPrice: 1000,
        shippingMethod: 'shipping',
        paymentId: 1,
        products: [
          { productId: 1, quantity: 10 },
          { productId: 2, quantity: 11 },
        ],
      });
    expect(res.status).toBe(200);
    expect(res.body.message).toEqual('Success');
  });
}, 9000);