데브코스 TIL - E2E 테스트 자동화
E2E 테스트 자동화
✅실습 내용
Headless browser를 이용한 테스트
테스트는 빈번하게 일어나기 때문에 이 때마다 사용자 인터페이스 요소들을 렌더링하는 것은 불필요한 오버헤드가 큼
selenium gird를 이용한 테스트 환경 구축
개발 단계에서 종단간 테스트의 결과를 눈과 손으로 확인하는데 활용
✅Headless browser?
브라우저의 동작을 포함하는 종단간 테스트의 자동화에 사용하는 GUI가 없는 브라우저
https://broswerstack.com/guide/what-is-headless-browser-testing
실습
- selenium IDE로 만들었던 로그인 테스트 로직 수정
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# Generated by Selenium IDE
import pytest
import time
import json
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.support import expected_conditions
from selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
class TestLogintest():
def setup_method(self, method):
self.driver = webdriver.Chrome()
self.vars = {}
def teardown_method(self, method):
self.driver.quit()
def test_logintest(self):
self.driver.get("http://localhost:3000/")
self.driver.set_window_size(1078, 1020)
self.driver.find_element(By.LINK_TEXT, "무료로 시작하기").click()
self.driver.find_element(By.ID, "emailInput").click()
self.driver.find_element(By.ID, "emailInput").send_keys("test@example.com")
self.driver.find_element(By.ID, "passwordInput").click()
self.driver.find_element(By.ID, "passwordInput").send_keys("1234")
self.driver.find_element(By.ID, "login-button").click()
- headless browser에 맞도록 수정한 파일
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# Generated by Selenium IDE
import pytest
import time
import json
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.support import expected_conditions
from selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
class TestLogintest():
def setup_method(self, method):
# headless browser 테스트 코드 추가
options = webdriver.ChromeOptions()
options.add_argument('--headless=new')
self.driver = webdriver.Chrome(options=options)
self.vars = {}
def teardown_method(self, method):
self.driver.quit()
def test_logintest(self):
self.driver.get("http://localhost:3000/")
self.driver.set_window_size(1078, 1020)
self.driver.find_element(By.LINK_TEXT, "무료로 시작하기").click()
self.driver.find_element(By.ID, "emailInput").click()
self.driver.find_element(By.ID, "emailInput").send_keys("test@example.com")
self.driver.find_element(By.ID, "passwordInput").click()
self.driver.find_element(By.ID, "passwordInput").send_keys("1234")
self.driver.find_element(By.ID, "login-button").click()
- 테스트 실행
1
python -m pytest
크롬창이 오픈되지 않고, 테스트가 진행됨
크롬창에 오픈하고 테스트하는 것보다 조금 빨리 테스트가 완료됨
셀레니움 그리드 활용 (standalone-chrome)
1. Docker-compose를 사용하여 테스트 환경 구축 (셀레니움)
기존에 만들어두었던 root 경로의 docker-compose.yaml
파일에 코드 추가
1
2
3
4
5
6
7
8
(...)
selenium:
image: selenium/standalone-chrome
ports:
- 4444:4444
(...)
이 때, 셀레니움에 의해서 테스트 접속할 때에는 다음과 같아야 함 (frontend, backend와 같은 네트워크에 존재하기 때문에)
http://localhost:3031
→http://backend:3031
http://localhost:3030
→http://frontend:3031
cross-site request에 의해 전달되는 cookie는 SameSite:'none'
,Secure:true
인 경우에만 받아들여짐. => 같은 네트워크에 웹 서버를 reverse proxy로 함께 구동하여 single origin이 되도록 설정 SameSite:'lax'
, Secure:false
1
2
3
4
5
6
7
8
9
10
11
12
13
res.cookie('access-token', accessToken, {
httpOnly: true,
...(process.env.NODE_ENV === 'development'
? {
sameSite: 'lax',
secure: false,
}
: {
sameSite: 'none',
secure: true,
}),
maxAge: 1000 * 60 * 60 * 24 * 14, // 14일
});
2. nginx reverse proxy 설정
notes.conf
Docker 컨테이너 안의 /etc/nginx/conf.d/default.conf
을 덮어 쓰도록 설정
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
server {
location / {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass http://frontend:3000/;
}
location /api/ {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass http://backend:3031/;
}
}
Dockerfile
1
2
3
FROM nginx:latest
COPY notes.cof /etc/nginx/conf.d/default.conf
3. docker-compose.yaml
에 네트워크 구성 코드 추가 및 수정
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
version: '3.8'
services:
notes:
build: ./notes-web
ports:
- 4000:80
networks:
- notes
frontend:
image: 'hyeem66/prgms-fe:latest'
environment:
REACT_APP_API_BASE_URL: http://notes/api
ports:
- 3000:3000
networks:
- notes
backend:
image: 'hyeem66/prgms-test:latest'
environment:
NODE_ENV: development
DB_HOST: db
DB_PORT: 3306
ports:
- 3031:3031
networks:
- notes
db:
image: mariadb:11.2.2
environment:
- MARIADB_ROOT_PASSWORD:root
# 로컬 컴퓨터 콘솔에서 접근하는 경우 - localhost:3032
# docker compose에 의하여 같은 네트워크에서 실행하는 컨테이너에서 접근 - db:3306
ports:
- 80:3306
networks:
- notes
# host:mysql경로 (volume 공유)
volumes:
- ./db:/var/lib/mysql
selenium:
image: selenium/standalone-chrome
ports:
- 4444:4444
networks:
- notes
networks:
notes: {}
도커 실행
1
2
3
4
5
6
7
8
docker compose up -d
[+] Running 5/5
✔ Container prgms-notes-frontend-1 Started
✔ Container prgms-notes-notes-1 Running
✔ Container prgms-notes-db-1 Started
✔ Container prgms-notes-backend-1 Running
✔ Container prgms-notes-selenium-1 Started
4. 테스트 코드 수정
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
# Generated by Selenium IDE
import pytest
import time
import json
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.support import expected_conditions
from selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
# BASE URL = "http://localhost:30030"
BASE URL = "http://notes"
class TestNotesView():
def setup_method(self, method):
# headless browser 설정
options = webdriver.ChromeOptions()
self.driver = webdriver.Remote(
command_executor = 'http://localhost:4444/wd/hub',
options=options
)
self.vars = {}
self.login()
def teardown_method(self, method):
self.logout()
self.driver.quit()
def test_notesview(self):
time.sleep(1)
self.driver.get(BASE URL + "/notes")
self.driver.set_window_size(1078, 1020)
self.driver.impicitly_wait(10)
# 로그인한 사용자의 ID 확인
assert self.driver.find_element(
By.ID, "current-user"
).text == "test@example.con"
# 노트 목록의 올바른 순서와 내용 확인
notes_list = self.driver.find_element(By.ID, "note-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)"
# self.driver.find_element(By.XPATH, "//div[@id=\'root\']/div/div/aside/div[3]/ul/li[last()]/a/span").click()
# 목록의 맨 아래 노트 제목 클릭
notes_list.find_element(By.XPATH, "ul[@id=\'note-list\']/li[last()]/a/span").click()
self.driver.impicitly_wait(2)
assert self.driver.find_element(
By.CSS_SELECTOR, "article header textarea"
).text == "Test (1)"
assert self.driver.find_element(
By.CSS_SELECTOR, "article main div div"
).get_attribute('innerHTML') == "<p>This note is for testing.</p>" \
+ "<p>Note number: 1</p>"
def login(self):
self.driver.get(BASE URL + "/")
self.driver.impicitly_wait(10)
self.driver.find_element(By.LINK_TEXT, "무료로 시작하기").click()
self.driver.find_element(By.ID, "emailInput").click()
self.driver.find_element(By.ID, "emailInput").send_keys("test@example.com")
self.driver.find_element(By.ID, "passwordInput").click()
self.driver.find_element(By.ID, "passwordInput").send_keys("1234")
self.driver.find_element(By.ID, "login-button").click()
def logout(self):
self.driver.find_element(By.ID, "logout-button").click()
This post is licensed under CC BY 4.0 by the author.