* 프로그래머스, 타입스크립트로 함께하는 웹 풀 사이클 개발(React, Node.js) 5기 강의 수강 내용을 정리하는 포스팅.
* 원활한 내용 이해를 위해 수업에서 제시된 자료 이외에, 개인적으로 조사한 자료 등을 덧붙이고 있음.
src/models/__mocks__/user.ts: 사용자 모델(User) 모킹__mocks__/jsonwebtoken.ts: JWT의 sign, verify, decode 함수를 모킹jest.config.js와 tsconfig.json을 통해 테스트 환경 설정// src/routes/users.test.ts
import request from "supertest";
import { app } from "../app";
import { User, MOCK_USERS } from "../models/__mocks__/user";
jest.mock("../models/user", () => jest.requireActual("../models/__mocks__/user"));
afterEach(() => {
MOCK_USERS.splice(0, MOCK_USERS.length);
});
describe("GET /users/me", () => {
test("올바른 JWT 쿠키가 설정되어있으면 유저 정보와 함께 200 응답을 받는다.", async () => {
MOCK_USERS.push(new User(1, "apple@example.com", "mock_encrypted_apple123"));
const response = await request(app)
.get("/users/me")
.set("Cookie", "access-token=mock_jwt_apple@example.com");
expect(response.status).toBe(200);
expect(response.body).toEqual({ email: "apple@example.com" });
});
});
@testing-library/jest-dom@testing-library/react@testing-library/user-eventpackage.json의 jest 설정에서 moduleNameMapper를 통해 별칭 설정src/utils/test/renderWithRouter.ts 내의 renderWithRouter() 함수src/setupTests.ts에서 window.alert() 모킹// src/components/JoinForm.test.tsx - Case 1: 렌더링
test("잘 렌더링된다.", () => {
renderWithRouter(<JoinForm />);
expect(screen.getByLabelText("이메일", { selector: "input" })).toBeInTheDocument();
expect(screen.getByLabelText("비밀번호", { selector: "input" })).toBeInTheDocument();
expect(screen.getByLabelText("비밀번호 확인", { selector: "input" })).toBeInTheDocument();
expect(screen.getByText("회원가입", { selector: "button" })).toBeInTheDocument();
expect(screen.getByText("로그인하기", { selector: "a" })).toBeInTheDocument();
});
// src/components/JoinForm.test.tsx - Case 2: 버튼 콜백
test("회원정보를 입력하고 회원가입 버튼을 누르면 onSubmit 콜백이 호출된다.", () => {
const onSubmit = jest.fn();
renderWithRouter(<JoinForm onSubmit={onSubmit} />);
fireEvent.change(screen.getByLabelText("이메일"), { target: { value: "foo@example.com" } });
fireEvent.change(screen.getByLabelText("비밀번호"), { target: { value: "1234" } });
fireEvent.change(screen.getByLabelText("비밀번호 확인"), { target: { value: "1234" } });
screen.getByText("회원가입", { selector: "button" }).click();
expect(onSubmit).toBeCalledWith({ email: "foo@example.com", password: "1234" });
});
// src/components/JoinForm.test.tsx - Case 3: 링크 이동
test("로그인하기 버튼을 누르면 로그인 URL로 이동한다.", () => {
renderWithRouter(<JoinForm />);
fireEvent.click(screen.getByText("로그인하기"));
expect(window.location.pathname).toBe("/login");
});
// src/components/JoinForm.test.tsx - Case 4: 입력 일치 검사
test("비밀번호 확인을 다르게 입력하면 alert 창이 뜨고 onSubmit 콜백이 호출되지 않는다.", () => {
const alertSpy = jest.spyOn(window, "alert");
const onSubmit = jest.fn();
renderWithRouter(<JoinForm onSubmit={onSubmit} />);
fireEvent.change(screen.getByLabelText("이메일"), { target: { value: "foo@example.com" } });
fireEvent.change(screen.getByLabelText("비밀번호"), { target: { value: "1234" } });
fireEvent.change(screen.getByLabelText("비밀번호 확인"), { target: { value: "123456" } });
screen.getByText("회원가입", { selector: "button" }).click();
expect(alertSpy).toBeCalledWith("비밀번호가 일치하지 않습니다.");
expect(onSubmit).not.toBeCalled();
});
# ${PROJECT_ROOT}/backend/Dockerfile
FROM node:18
WORKDIR /var/app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
COPY ./build/ ./build/
ENV PORT=3031
# CORS_ALLOWED_ORIGIN=(...)
EXPOSE 3031
HEALTHCHECK CMD curl --fail http://localhost:3031/healthcheck || exit 1
ENTRYPOINT ["node", "."]
REACT_APP_ 으로 시작하는 환경 변수를 빌드 시 window._ENV 객체에 주입하기 위해, entrypoint 스크립트(docker-entrypoint.sh)를 작성# ${PROJECT_ROOT}/frontend/docker-entrypoint.sh
#!/bin/bash
set -e
echo -n "" > ./build/env.js
echo "window._ENV={" >> ./build/env.js
for key in $(compgen -v | grep ^REACT_APP_); do
echo "$key:'${!key}'," >> ./build/env.js
done
echo "}" >> ./build/env.js
exec serve -s build
# ${PROJECT_ROOT}/frontend/Dockerfile
FROM node:18
WORKDIR /var/app
RUN npm install -g serve
COPY build ./build
EXPOSE 3000
COPY docker-entrypoint.sh /usr/local/bin/
RUN chmod u+x /usr/local/bin/docker-entrypoint.sh
ENTRYPOINT ["docker-entrypoint.sh"]
window._ENV)을 통한 런타임 설정 중요env.js 파일 생성 및 동적 환경 변수 적용https://notes.prgms-fullcycle.com/api) 사용http://localhost:30030http://localhost:30031notes-db.db:3306) 이용DB_HOST, DB_USER, DB_PASSWD, DB_NAME, CORS_ALLOWED_ORIGIN 등을 포함하여 설정apiVersion: apps/v1
kind: Deployment
metadata:
name: notes-be
namespace: prgms-notes
spec:
replicas: 1
selector:
matchLabels:
run: notes-be
template:
metadata:
labels:
run: notes-be
spec:
containers:
- name: notes-backend
image: <Image to pull>
imagePullPolicy: Always
envFrom:
- configMapRef:
name: notes-be-config
---
apiVersion: v1
kind: Service
metadata:
name: notes-be
labels:
run: notes-be
namespace: prgms-notes
spec:
type: NodePort
selector:
run: notes-be
ports:
- port: 3031
targetPort: 3031
nodePort: 30031
apiVersion: apps/v1
kind: Deployment
metadata:
name: notes-fe
namespace: prgms-notes
spec:
selector:
matchLabels:
run: notes-fe
replicas: 1
template:
metadata:
labels:
run: notes-fe
spec:
containers:
- name: notes-frontend
image: <Image to pull>
imagePullPolicy: Always
ports:
- containerPort: 3000
env:
- name: REACT_APP_API_BASE_URL
value: http://localhost:30031
---
apiVersion: v1
kind: Service
metadata:
name: notes-fe
labels:
run: notes-fe
namespace: prgms-notes
spec:
type: NodePort
selector:
run: notes-fe
ports:
- port: 3000
nodePort: 30030
make push, make deploy, make undeploy 등을 통해 이미지 빌드 및 k8s 적용Docker를 이용해 Selenium standalone 컨테이너(예: selenium/standalone-chrome)를 실행합니다.
docker run -d --rm -p 4444:4444 -v /dev/shm:/dev/shm selenium/standalone-chrome
Python 라이브러리 설치 후, 간단한 테스트 코드를 작성하여 원격 WebDriver에 접속하고 브라우저 자동화를 수행합니다.
from selenium import webdriver
import time
print("Test Execution Started")
options = webdriver.ChromeOptions()
options.add_argument('--ignore-ssl-errors=yes')
options.add_argument('--ignore-certificate-errors')
driver = webdriver.Remote(
command_executor='http://localhost:4444/wd/hub',
options=options
)
# 최대화 및 테스트 페이지 접속
driver.maximize_window()
time.sleep(10)
driver.get("https://notes.prgms-fullcycle.com")
time.sleep(10)
driver.find_element("link text", "무료로 시작하기").click()
time.sleep(10)
# 브라우저 종료
s = input("Done: ")
driver.close()
driver.quit()
print("Test Execution Successfully Completed!")
.side 파일로 저장되며, command-line runner(예: selenium-side-runner)를 통해 재생할 수 있습니다.import pytest
from selenium import webdriver
from selenium.webdriver.common.by import By
class TestUntitled:
def setup_method(self, method):
self.driver = webdriver.Chrome()
self.vars = {}
def teardown_method(self, method):
self.driver.quit()
def test_untitled(self):
self.driver.get("http://localhost:30030/")
self.driver.set_window_size(931, 685)
self.driver.find_element(By.LINK_TEXT, "무료로 시작하기").click()
self.driver.find_element(By.NAME, "email").send_keys("test@example.com")
self.driver.find_element(By.NAME, "password").send_keys("1234")
self.driver.find_element(By.ID, "login-button").click()
self.driver.find_element(By.CSS_SELECTOR, "#logout-button > span").click()
self.driver.close()
login() 및 logout() 메서드를 정의하여 모든 테스트에서 공통으로 사용하는 동작을 라이브러리화합니다.def test_loginlogout(self):
self.driver.get("http://localhost:30030/")
self.login()
self.driver.implicitly_wait(10)
self.logout()
def login(self):
self.driver.find_element(By.LINK_TEXT, "무료로 시작하기").click()
self.driver.find_element(By.NAME, "email").send_keys("test@example.com")
self.driver.find_element(By.NAME, "password").send_keys("1234")
self.driver.find_element(By.ID, "login-button").click()
def logout(self):
self.driver.find_element(By.CSS_SELECTOR, "#logout-button > span").click()
def test_noteview(self):
import time
time.sleep(1)
self.driver.get(BASE_URL + "/notes")
self.driver.implicitly_wait(10)
assert self.driver.find_element(By.ID, "current-user").text == "test@example.com"
notes_list = self.driver.find_element(By.ID, "notes-list")
assert notes_list.find_element(By.CSS_SELECTOR, "li:nth-child(1) span").text == "Test (2)"
assert notes_list.find_element(By.CSS_SELECTOR, "li:nth-child(2) span").text == "Test (1)"
notes_list.find_element(By.XPATH, "li[last()]/a/span").click()
self.driver.implicitly_wait(2)
assert self.driver.find_element(By.CSS_SELECTOR, "article header textarea").text == "Test (1)"
expected_content = "<p>This note is for testing.</p><p>Note number: 1</p>"
assert self.driver.find_element(By.CSS_SELECTOR, "article main div div").get_attribute("innerHTML") == expected_content
def test_loginfail(self):
self.driver.get("http://localhost:30030/")
self.driver.find_element(By.LINK_TEXT, "무료로 시작하기").click()
self.driver.find_element(By.NAME, "email").send_keys("test@example.com")
self.driver.find_element(By.NAME, "password").send_keys("1235")
self.driver.find_element(By.ID, "login-button").click()
assert self.driver.switch_to.alert.text == "이메일 또는 비밀번호가 일치하지 않습니다."
ChromeOptions를 이용해 headless 모드로 실행할 수 있습니다.server {
location / {
proxy_set_header Host $host;
proxy_pass http://frontend:3000/;
}
location /api/ {
proxy_set_header Host $host;
proxy_pass http://backend:3031/;
}
}