안녕하세요 씨알에스큐브 사업 1팀에서 개발하고 있는 개발자 임준영입니다.
저희 팀은 cubeCDMS, cubeIWRS, cubePRO를 설정할 때 사용하는 솔루션 cubeBUILDER, EDC 솔루션 cubeCDMS, IP 관련 통합 관리 솔루션 IWRS 등을 담당으로 하고 있는 팀입니다. 솔루션에 대한 더 자세한 이야기는 하기 링크를 참조 부탁드립니다.
cubeBUILDER, cubeCDMS, cubeIWRS
1팀에서는 저희의 솔루션이 연혁이 있는 솔루션인 만큼, 솔루션 안정화에 대한 노력과 성능 개선에 대한 노력과 고민을 항상 하고 있습니다.
그래서 오늘 소개해 드릴 이야기는 평소 제가 관심 갖던 분야인 E2E 테스트에 대한 이야기와 입사 후 시도한 E2E 자동화 테스트 대한 이야기를 전해드리려 합니다.
(해당 내용은 2022년 2월 cube Dev Tech Seminar에서 발표한 'E2E 자동화 도구 Cypress를 이용한 CRF page unit test'에 대한 내용을 담고 있습니다.)
E2E 테스트란?
End to End 테스트의 준말로 개발물을 사용자 관점에서 테스트하는 방법을 이야기합니다.
예를 들어 페이지에서 원하는 텍스트가 제대로 출력되었는가?, 버튼을 클릭했을 때 올바른 동작을 수행하는가? 등을 테스트할 수 있는 방법입니다.
위 내용만 들었을 때는 '그냥 Unit Test랑 다른 게 뭐야?'라는 생각이 들지도 모릅니다.
보충 설명을 더하고 필요 의의를 설명하도록 하겠습니다.
E2E와 유닛 테스트의 차이점은 위에서 말했다시피 사용자 관점이라는 키워드에서 큰 차이를 가집니다.
1+1 기능을 개발할 때 개발자 관점에서는 당연히 2가 나와야 하는 것이 정상작동일 것입니다.
그러나, 사용자 관점에서 봤을 때 2가 아닌 3이 나와야 한다면 위에서 구현한 기능은 오작동이며 이는 명백히 버그입니다.
작성된 코드에 따라 정상작동을 확인하는 유닛 테스트는 통과되어도 직접 사용하는 사용자 관점에서 테스트하는 E2E 테스트 케이스에서는 이는 실패한 케이스로 남겨지는 것이죠
이렇듯 관점 차이에 따른 테스트이기 때문에 양쪽 모두 중요한 테스트임은 분명합니다.
그럼 이러한 테스트들은 왜 필요할까요?
왜 필요한가?
시스템을 변경하는 방법은 총 2가지가 존재한다.
편집 후 기도하기, 보호 후 수정하기
-마이클 C. 페더스 저자, 레거시 코드 활용 전략 중 1장 소프트웨어 변경.-
오래된 레거시 애플리케이션들 외에도 다양한 웹 서비스, 애플리케이션들은 다양한 시스템, DB와 연결되고 통합되어 있습니다.
그로 인해 애플리케이션들의 작업흐름(workflow)은 보다 복잡해졌고, 다른 이슈임에도 서로 영향을 주고받는 관계가 만들어지기 쉽습니다.
이렇듯 우리는 서로 영향을 주고받는 이러한 관계에서 발생하는 이슈들을 확인하기 위해 E2E 테스트를 진행합니다.
E2E 테스트는 다양한 앱의 의존관계가 정확히 작동하는지 확인하는 방법이기에 어느 애플리케이션이든 필요한 테스트 기법 중 하나인 것이죠 이외에도 정확한 정보가 다양한 시스템 컴포넌트 사이에서 전달되는지 체크할 수 있는 부가 장점 또한 지니고 있습니다.
E2E 테스트를 진행하면서 흔히들 착각하는 것 중 하나는 '내가 수정한 이슈가 해결되었는가?'를 중점적으로 생각하지만, 실제로는 '내가 수정함으로 다른 화면에서 이슈가 없는가?'를 좀 더 중점적으로 생각해야 하는 것입니다.
이는 개발자가 의도한 바로 테스트하는 것(내가 B페이지를 수정했으니 A페이지는 영향이 없겠지?)이 아닌 진짜 사용자의 관점(B페이지를 들어가기 위한 A페이지에 대한 테스트)에서 진행해야 하는 것도 생각해야 합니다.
때문에, 개발자들은 자동화 테스트라는 것을 고민해야 할 시기가 찾아옵니다.
왜냐하면 이러한 E2E 테스트를 일일이 개발자가 지속적으로 한다면 그것은 분명히 시간 낭비의 일종이기 때문이죠
그리고, 테스트가 존재하지 않을 때 레거시 코드를 수정했을 때 그 수정으로 인한 영향은 아무도 모르는 것이기 때문입니다. (전지전능하신 신도 알 수 없는 것이 저와 같은 주니어 개발자의 코드 아닐까요?)
사실상 테스트 자동화는 프로젝트를 시작했을 때부터 같이 붙이는 것이 제일 베스트이긴 하지만, 저는 그래도 최대한 빠르면 빠를수록 좋은 것이 테스트 자동화라고 생각합니다.
이러한 테스트들을 손수 하게 되면 테스트 비용이라는 것이 발생하기 때문입니다.
테스트 비용
먼저 사람이 하는 테스트 비용은 테스트의 종류 상관없이 매우 높다는 것이 특징입니다.
심지어 E2E 테스트는 개발자가 의도한 상황에 대한 테스트만 수행하는 것이 아니기에 더 높을 수밖에 없습니다.
현재 제가 담당하고 있는 솔루션에서는 백엔드 환경에서만 자동화 테스트가 존재하고, 프론트 환경의 테스트는 모두 사람이 진행하는 테스트만 존재합니다. 이조차 E2E 테스트이기 때문에 비용이 높게 책정되는 경향이 존재하죠
테스트의 높은 비용은 결국 테스트 케이스를 줄이게 되고, 그로 인해 놓치는 이슈들을 발생시키는 원인이 되기도 합니다.
그럼 어떻게 이 현상을 해결할 수 있을까요?
그에 대한 해답은 매우 간단합니다. 프론트 환경에서도 자동화 테스트 환경을 만드는 것입니다.
단순 반복하는 테스트 케이스라도 커버되는 수준의 자동화 테스트가 존재한다면, 개발자가 수행하는 테스트 비용은 크게 줄어들 수 있을 것이라고 생각했고, 직접 솔루션에 적용해본 사례를 이야기 하기 이전에 테스트 자동화와 자동화 도구에 대해서 얘기하고 적용 사례를 짧게 설명하도록 하겠습니다.
테스트 자동화
테스트는 결국 정형화된 케이스가 존재하기 마련입니다.
그렇다는 것은 어느 정도의 케이스는 단순 반복 액션을 취하는 테스트들이 있다는 것이고, 그 테스트를 자동화한다면 테스트 비용은 눈에 띄게 감소할 것이라는 예측은 자동화를 시도할 계기가 되었고 이것은 굉장히 좋은 아이디어라고 생각했었습니다.
큰 범위의 테스트 시나리오를 구성하고, 해당 시나리오를 자동화로 구축해 코드 수정 후 개발자가 한 번씩 실행해 간단히 영향도를 확인할 수 있는 도구로서 활용할 수 있도록 사용 사례가 많은 자동화 도구인 Cypress를 사용해 시도해봤습니다.
Cypress
The web has evolved. Finally, testing has too.
웹이 진화 한 만큼 테스트 또한 진화했습니다.
- Cypress -
Cypress README에서 맨 처음 볼 수 있는 Cypress 슬로건입니다.
이런 슬로건 하나부터 테스트 도구로서 상당한 자부심을 가지고 있다는 것을 알 수 있었습니다.
상당한 자부심을 증명하듯 Cypress는 7년 전 처음 공개되어 현재까지 꾸준히 사랑받으며, 상당한 인지도를 자랑합니다.
Cypress를 선택한 이유
E2E 테스트 자동화 도구는 종류가 다양했지만, 그중에서도 가장 언급이 많았던 도구 3가지(Cypress, CodeceptJS, TestCafe)를 비교해 봤지만 2가지 이유로 Cypress를 사용하기로 결정했습니다.
1. 다운로드 수
다운로드 수가 많다는 것은 그만큼 사용자가 존재한다는 것을 알 수 있었고
이는 사용 사례, 공식 문서의 볼륨, 안정성을 뒷받침하는 근거가 될 수 있죠
실제로, TestCafe는 비교적 작성된 글이 있는 반면에 CodeCeptjs는 한국어로 작성된 글을 찾아보기 어려웠었습니다.
2. 케이스 작성 용이성
- Cypress에서 제공하지 않는 함수에 대해서는 직접 만들어 사용할 수 있다.
예를 들면, 존재하는 Element 한에서 클릭 이벤트를 실행하거나 솔루션에 로그인하는 등의 원하는 방향의 테스트를 만들어 나갈 수 있었습니다. - 이벤트를 발생시켜야 하는 Element를 손쉽게 탐색할 수 있다.
타 E2E 테스트 툴 경우에는 개발자 도구에서 원하는 Element를 찾아야 하는 방법을 사용해야 하지만, Cypress는 프로그램에서 클릭 몇 번으로 로그인을 위해 입력하는 id 칸의 Element Id 값을 얻을 수 있습니다.
그렇다면 단점은?
이 세상의 모든 것이 완벽한 사람이 없듯이 Cypress 또한 단점이 존재했습니다.
단점 또한 2가지 정도 소개해 드리도록 하겠습니다.
1. UA(User Agent) 설정이 까다롭다.
웹 서비스라고 해도 모바일 접속을 고려한 웹 서비스라면, 모바일에서도 정상 작동 여부를 확인하는 것이 당연하겠죠?
그래서, Cypress 또한 모바일 화면에서도 테스트할 수 있도록 UA를 지원하고 있지만, 해당 설정이 까다롭다는 점이 상당히 단점으로 다가온다는 이야기가 많았습니다.
그저 화면을 보기 위한 모바일 설정이라면 'viewport' 함수를 사용하여 해상도에 따른 화면을 확인 가능하지만,
User Agent를 통한 핸들링을 확인해야 하는 서비스라면 하기 Json처럼 따로 설정 파일을 관리해야 하기 때문에 많은 모바일 기기를 고려해야 하는 웹 서비스라면 상당히 많은 Json파일이 필요하니 여간 귀찮은 작업이 아닐 수 없습니다.
{
"userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_1 like Mac OS X) AppleWebKit/603.1.30 (KHTML, like Gecko) Version/10.0 Mobile/14E304 Safari/602.1",
"viewportWidth": 375,
"viewportHeight": 812
}
2. async / await의 부재
사용자들이 가장 크게 생각하는 단점 중 하나는 async / await를 지원하지 않는다는 단점이 있었습니다.
역시 많은 사람들이 불만을 가지는 이슈였는지 Cypress 페이지 FAQ 탭에서 Cypress의 의견이 작성되어 있는 점을 확인할 수 있었습니다.
간단히 요약하자면, '*Commadns API는 async / await 기능이 작동하지 않는 방향으로 설계하였다.'라는 의견이었고,
(*Cypress에서 사용하는 cy.get, cy.click과 같은 테스트 액션을 수행하는 API)
'해당 설계는 의도된 바이다.'라는 설명을 추가로 하며, 추후 async / await 지원에 대해 하지 않을 것 이라는 분위기를 표현했죠
3. 직접 사용하며 느낀 주관적인 단점 간단히
그리고 테스트 환경을 구성하면서 든 단점 중 하나는 수행 속도가 생각보다 느리다는 점과 테스트 타이밍 맞지 않는다는 단점을 느끼기도 하였습니다. 하지만 이는 주관적인 생각일 수 있기 때문에 자세하게 다루지는 않도록 하고, 사용하면서 '이런 기능이 있었어?'라는 생각이 들었던 신기한 기능을 하나 소개해 드려 합니다.
Cypress의 Intercept 기능
Cypress 도입을 위해 공부하면서 제일 신기했던 기능이라 주변 동료분들에게 많이 홍보하기도 했던 Intercept라는 기능을 간단히 소개해보려 합니다.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>
</head>
<body>
<h1 id="titleText">제목</h1>
<script>
$.ajax({
type: 'POST',
url: 'http://localhost:3000/intercept/test',
data: JSON.stringify({'delFlag': true, 'text': '테스트 성공'}),
success: data => {
$('#titleText').text(data.text);
},
error: err => {
$('#titleText').text(err.statusText);
}
});
</script>
</body>
</html>
위와 같이 간단하게 response에서 받은 텍스트를 화면에 띄워주는 코드를 구성했을 때 백엔드에서는 하기와 같은 코드를 작성해야 할 것이며, 실제 서버에 띄워주는 작업이 필요하겠죠?
// app.js
const express = require('express');
const app = express();
const cors = require('cors');
const port = process.env.PORT || 3000;
const bodyParser = require('body-parser');
app.use(bodyParser.urlencoded({extended: false}));
const main = require('./router/main');
app.use(cors());
app.use('/', main);
app.listen(port, () => {
console.log(`Server On! ${port}`);
});
// main.js -------------------------------------------------------------------------
const express = require('express');
const router = express.Router();
router.get('/', (req, res) => {
res.json({text: 'Ajax'});
});
router.post('/intercept/test', (req, res) => {
const { delFlag, text } = req.body;
if(delFlag) res.json({text})
});
module.exports = router;
하지만, Cypress에서는 하기와 같이 테스트 코드에서 Intercept를 사용해 미리 API 인터페이스를 구성해 놓으면 위 백엔드 코드 없이 쉽게 테스트를 할 수 있습니다.
/// <reference types="cypress" />
describe('test Demo', () => {
context('테스트', () => {
it('테스트입니다.', () => {
cy.intercept({
method: 'POST',
pathname: '/intercept/test',
}, req => {
const { delFlag, text } = JSON.parse(req.body);
if (delFlag) req.reply({ text });
});
cy.visit('http://localhost:5500/index.html');
cy.get('#titleText').should('have.text', '테스트 성공');
});
});
});
위 'cy.intercept'에서 API 메서드(method)와 경로(pathname)를 객체로 파라미터로 넘기고 콜백으로 해당 API를 호출했을 때 text를 반환해주도록 하는 간단한 API 인터페이스를 구현하면, 간단하게 API 서버 없이 API로 넘어오는 데이터로 처리하는 화면 테스트가 가능합니다.
해당 기능을 활용한다면 백엔드 개발자의 도움 또는 API 서버 없이 프론트 개발자 혼자서 테스트 코드만 존재한다면, 쉽게 테스트가 가능하다는 장점을 가질 수 있습니다.
물론 이를 위해서는 프론트와 백엔드 사이 API 인터페이스 논의가 이루어진 이후여야 합니다.
이외에도 테스트 녹화, Element 확인 기능 등 유용한 기능들이 많지만 실제 적용 사례를 소개하기 위해서 이쯤 하고 넘어가도록 하겠습니다.
실제 적용은?
실제 적용 사례는 개인적으로 간단히 설정한 eCRF 과제에서 진행하였습니다.
간단하게 CDMS에서 지원하는 화면 액션 중 eCRF 화면에서 원하는 화면을 블라인드 하는 간단한 로직을 실험하는 테스트였지만, 내부 로직이 생각보다 꼬여 있어 항상 이슈가 발생하는 병목 지점입니다.
해당 이슈를 자동화 테스트로 어느 정도 커버할 수 있다면, 개발자가 수정했을 때 비교적 안전하게 수정이 가능할 것 같았기에 우선순위를 높여 적용을 시도했습니다.
테스트를 자동으로 한다고 하여도 저희는 테스트를 했다는 증거 자료가 항상 필요하죠?
그래서 저는 Cypress에서 it 단위 테스트가 끝날 때마다 수행하는 동작을 정의하는 afterEach hook을 이용해 it 단위 테스트가 끝날 때마다 화면 사진을 저장하도록 설정하여 시나리오를 구성하였습니다.
afterEach(() => {
cy.waitSec(1.5);
cy.screenshot(imageName);
});
위에 있는 사진의 경로는 사전에 Cypress 설정 파일에서 임의 폴더에 저장되도록 설정이 가능합니다.
(저는 하기와 같이 config를 설정하였습니다.)
// cypress.config.js
module.exports = {
"viewportWidth": 1920,
"viewportHeight": 1080,
"defaultCommandTimeout": 4000,
"pageLoadTimeout": 30000,
"video": true,
"videoCompression": 15,
"experimentalSessionSupport": true,
"integrationFolder": "./integration/test",
"screenshotsFolder": "./integration/screenshots",
"videosFolder": "./integration/videos",
"chromeWebSecurity": false
}
그리고, 이때까지 간단한 설정들과 기능들을 설명드렸으니 실제로 동작하는 화면도 필요할 것 같아 짧게 gif로 담아서 첨부하였습니다.
위와 같이 본래, 개발자가 직접 일일이 해야 하는 대상자 초기화(=Mock 데이터 초기화), 블라인드 기능의 트리거 동작을 자동화된 시나리오 안에서 실행해주기 때문에 블라인드 로직 관련 수정을 했을 때 개발자는 해당 테스트 시나리오를 사용한다면 확인해야 할 범위가 매우 줄어들 수 있는 것이죠.
그리고 지속적으로 해당 테스트 시나리오를 세분화하는 작업을 프로세스에 추가하여, 테스트 범위를 확장해 나간다면 충분히 큰 역할을 할 수 있는 테스트 시나리오가 작성될 것입니다.
그러나, 현재 자동화 테스트 도입은 암묵적 홀딩 상태에 이르렀습니다.
그 이유는 무엇일까요?
- 개발자의 업무를 줄이기 위함이지만, 오히려 개발자의 업무가 증가되고 있다.
이는 당연할 수 있지만, 테스트 시나리오에 대한 프로세스를 확립하게 된다면 이는 온전히 개발자의 업무 증가로 이어질 수 있습니다. 테스트 시나리오 관리에 대한 프로세스, 시나리오 내용 추가에 대한 프로세스, 시나리오 내용 제거에 대한 프로세스 등 자칫 잘 못한다면 수많은 관리 포인트가 생겨버리는 문제점이 발생했습니다. - 시나리오 실행 시간이 느리다.
테스트 시나리오 실행 시간이 생각보다 느리다는 것입니다.
이미 정확한 범위가 지정되어 있고, 프로세스가 정해져 있어 정상 적인 자동화 테스트가 존재한다면 이를 돌려봄으로써 정상 작동을 확인하는 멋진 상황이 펼쳐지겠지만 현재로써는 도입 단계이다 보니 비효율적인 시나리오가 생산되어 사용되었기 때문에 개발자가 원하는 결과를 얻기 위해서는 비효율적인 시나리오를 작동하기에 실행하기 꺼려진다는 평가가 많았습니다. - 화면 정상작동에 대한 범위 명확히 명시되어 있는 것이 없다.
자동화 테스트의 시나리오 작성을 위해서는 정상 작동에 대해서 명확히 명시된 것이 필요합니다.
그러나, 솔루션을 유지 보수하면서 이전 개발자 분들께서 화면 작동에 대해 정상 작동을 명확히 명시해놓은 것이 없었고 사용자 자유도가 높은 솔루션이기 때문에 이 범위 또한 굉장히 넓은 것이 문제였습니다. 해당 문제가 자동화 테스트 도입이 홀딩된 가장 큰 이유라고 봐도 무방할 것 같습니다.
마무리
위와 같은 이유와 더불어 추가적인 이유로 기나긴 2개월의 도입기가 마무리 되었습니다.
이는 분명히 제게 테스트 관련 지식이 부족했음을 깨닫게 해 준 큰 경험이 되기도 하였습니다.
테스트 시나리오를 작성하면서, '어? 이건 정상작동인가? 아니면 비정상 작동인가?'라는 생각이 항상 들었던 적도 많았답니다.
그 이유는 화면의 정상 작동이 명확히 정의되지 않은 점도 크지만, 프론트 코드가 설계된 지 오래되어 요소들의 규칙들을 알기 어려워 발생하기도 하였습니다.
그래도 짧은 시나리오들을 작성함으로 화면 설계를 이해할 수 있었고, 이는 추후 화면 개발할 때 큰 도움이 되었죠
개인 적으로 처음 들어온 개발자가 화면 개발을 하기 전에 한 번씩 시나리오를 작성하도록 프로세스를 만들고 싶은 생각이 들 정도로 말이죠
그리고 완전히 끝난 것이 아닌 홀딩 상태이기 때문에 충분히 제 테스트 지식이 쌓였을 때 도전할 수 있는 기회는 존재합니다. (넘치는 것이 주니어 개발자의 시간이기 때문이죠 ㅎㅎㅎ....)
그리고 추후 정상 작동만 정의된다면, cubeBUILDER를 사용해 테스트 시나리오 작성 자유도가 상당히 높아 케이스를 제가 만들어가며 할 수 있어 시나리오 작성 난이도는 비교적 여타 다른 솔루션에 비해서는 쉽다는 점도 있어 충분히 도입 가능성을 볼 수 있는 도입기였습니다.
이때까지 횡설수설했던 제 긴 이야기를 들어주셔서 감사합니다. :)