주제 | 대상 |
---|---|
IDE | VSCode |
언어 | TypeScript |
실행 (빌드) | Jenkins |
테스트 로그 관리 | NHN Cloud Object Storage |
테스트 결과 알림 | NHN Dooray Hook |
E2E 테스트를 위해 고려되었던 것들은 위의 표 내용 정도로 간단하게 정리가 된다.
나는 IntelliJ를 좋아하지만,
VSCode의 Extension에서 Playwright 확장 프로그램이 설치가 가능한 것을 보고 바로 VSCode로 넘어왔다. 테스트 실행은 GUI로 하는 것이 굉-장히 편리하기 때문에..!!
나중에 찾아보니 Jetbrains에서 테스트를 위한 IDE인 Aqua 베타 버전을 무료로 제공해주고 있다는 것을 알고 한번 써볼까 했는데.. 아쉽게도 현재는 셀레니움까지만 지원을 해주며 Playwright는 준비 중이라고 해서 VSCode로 시작을 했다.
이번 프로젝트 덕분에 TypeScript를 처음 접해보았다.
TypeScript 언어 자체의 장점도 많아서 한번쯤 도전해보고 싶어서 다른 언어와 크게 비교하면서 깊게 고민하지는 않았던 것 같다.
그리고.. 솔직하게 말하면, 아직 Playwright에 대한 커뮤니티가 크게 활성화되어 있지 않다.
그나마 활성화된 커뮤니티에서 TypeScript로 Playwright를 사용하여 개발한 사례가 비중이 높았기 때문에 좀더 편리한 개발을 위해 선택하였다. 주니어인 나에게는 같은 언어로 고민하는 사람들의 이야기를 보는 것이 훨씬 이해도 빠르고 효율도 좋을 것이라고 생각했다.
이번 E2E 프로젝트의 최종 목표는 QA 서버 배포 직후 자동으로 E2E 테스트까지 실행되는 것.
그래서 빌드 프로세스에 E2E 실행까지 포함되어야 했기 때문에 Jenkins 파이프라인 설계까지 작업이 필요했다.
젠킨스에서 테스트 실행을 목적으로 하는 빌드가 진행될 때, 미리 빌드된 도커 이미지를 실행함으로써 테스트가 돌아가게 된다. 테스트가 끝난 직후, 테스트 로그를 확인하기 위해 직접 도커 컨테이너에 접근하여 로그 파일을 찾아 들어가는 과정은 매우 불편하고 비효율적이라 테스트가 종료되면 로그를 한 곳으로 자동으로 업로드하여 관리를 해줄 필요가 있었다.
결론적으로 NHN Cloud에서 제공하는 Object Storage에 로그를 올려서 확인 가능하도록 하였다.
테스트 시작, 테스트 종료, 테스트 성공 및 실패 여부에 대해 알람을 실시간으로 받을 필요가 있었다. 알람을 받을 창구로 사내 메신저로 사용 중인 NHN Dooray의 Hook API를 사용하기로 하였다.
MVC, Singleton 등의 패턴은 알고 있었지만
막상 "E2E 테스트를 위한 디자인 패턴..?" 을 생각하니 어떤 것을 참고해야 하나 고민이 컸다.
https://playwright.dev/docs/pom
Playwright 공식 문서에서는 POM 모델을 추천했고,
Celenium 등 E2E 테스트를 위해 다른 라이브러리를 사용하는 경우에도 일반적으로 POM 모델을 따르고 있었다.
간단하게는 테스트가 진행되는 Page와,
Page를 구성하는 Object로 나뉘어 설계를 한다고 생각하면 된다.
POM 모델을 바탕으로 테스트 케이스를 추가하며 디렉토리 구조를 설계하기 시작했다.
그리고.. 정말 많이 고민하고 많이 갈아엎었다.
대략적인 구조는 아래와 같다.
src
├── admin
└── front
├── page-locators
│ ├── mobile
│ └── pc
│ ├── login
│ │ ├── ILoginLocators.ts
│ │ ├── LoginPage.ts
│ │ ├── skinA
│ │ │ └── LoginLocators.ts
│ │ └── skinB
│ │ └── LoginLocators.ts
│ └── ...
└── paths
└── PagePaths.ts
Depth | 대상 | 구분 이유 |
---|---|---|
1 | 환경 | 환경에 따라 테스트가 아예 달라짐 (pc / mobile) |
2 | 서비스 | 스킨에 따라 제공되는 서비스가 다르지 않음, 스킨이 달라도 동일한 Page를 공유함 |
3 | 스킨 | 위(2)와 동일 |
Page, Object 세팅과 관련된 요소들을 src 하위로 위치시켰다면,
테스트 실행 전후, 테스트에서 전반적으로 활용되는 요소들은 common 하위로 위치시켰다.
☑️ common/data
common
├── data
│ └── ...
static 변수로 값을 저장하여 프로젝트 전반에서 활용되어야 하는 data 정보를 관리하는 곳이다.
테스트 실행 시, 테스트 정보에 대한 파라미터는 env 값으로 전달 받는데,
env 값을 static 변수로 저장하여 프로젝트 전반에서 활용할 수 있도록 해야했다.
지금은 이 역할을 common/data/**
에서 담당하고 있다.
// common/data/env/Domain
export class Domain {
static hasDomain: boolean = process.env.DOMAIN !== undefined
static DOMAIN: string = Domain.hasDomain ? process.env.DOMAIN! : ""
static SUBDOMAIN: string = Domain.hasDomain ? (new URL(process.env.DOMAIN!).hostname!).split('.')[0] : ""
// ... 생략
}
☑️ common/external
외부 서비스와의 연동이 필요한 경우가 많았다.
Dooray Hook 알람, Jenkins 빌드, OBS 업로드 등등..
외부 서비스와 api로 통신하는 곳은 external 폴더 하위로 분류하였다.
├── external
│ ├── dooray
│ ├── jenkins
│ └── obs
☑️ common/fixture
├── fixture
│ ├── Fixture.ts
│ └── (domain)
│ ├── Member.json
│ └── ...
테스트에 필요한 실제 데이터 값을 보관하고, 읽는 곳이다.
Fixture 클래스는 json 파일을 읽어 원하는 데이터를 array로 반환해주는 역할을 담당한다.
import { Domain } from '@common/data/env/Domain'
import * as fs from 'fs'
export class Fixture {
static get(fileNm: string, key: string) {
const jsonFile = fs.readFileSync(`common/fixture/${Domain.SUBDOMAIN}/${fileNm}.json`, 'utf8')
const fixture = JSON.parse(jsonFile)[key]
return fixture
}
}
예를 들어 Memeber.json 파일 내에는 아래와 같이 데이터가 저장되어 있고,
{
"testUser": {
"id": "(id)",
"name": "(name)",
"pw": "(password)"
},
...
}
실제로 값을 읽을 때는 아래와 같이 호출한다.
const fixture = Fixture.get("Member", "testUser")
☑️ common/hook
├── hook
│ ├── CustomReporter.ts
│ ├── afterEach
│ │ └── ...
│ └── beforeEach
│ └── ...
Playwright에서 기본적으로 제공해주는 Reporter 인터페이스를 implements하면
본인이 원하는 커스텀 레포트를 만들 수 있다.
export default class CustomReporter implements Reporter {
// 전체 테스트 시작 전
onBegin(config: FullConfig, suite: Suite): Promise<void> { }
// 전체 테스트 종료 후
onEnd(result: FullResult): Promise<void> { }
// 개별 테스트 시작 전
onTestBegin(test: TestCase, result: TestResult): void { }
// 개별 테스트 종료 후
onTestEnd(test: TestCase, result: TestResult): void { }
}
각 케이스에 따라 적절하게 로그나 외부 api 호출 등을 추가하여 테스트 실행 결과 관리를 하였다.
☑️ common/log
├── log
│ └── Logging.ts
이번 프로젝트에서는 winston로 로그 관리를 하였다.
로그 작성에 대한 소스코드는 위 클래스로 관리하였다.
☑️ common/page
├── page
│ ├── Admin
│ │ └── AdminPageCommon.ts
│ ├── Front
│ │ ├── MobilePageCommon.ts
│ │ └── PcPageCommon.ts
│ └── PageCommon.ts
Page 관리에 대해 Common한 요소들만 모아놓은 곳이다.
Page 공통적으로 필요한, 특정 페이지를 여는 것을 Page.open()
이라는 하나의 메서드로 통일시키기 위해 고민한 결과이다. 이건 별도의 포스팅으로 정리하는 것이 좋을 듯 싶다.
➡️ (포스팅 완성 후, 여기에 링크가 생성될 예정입니다.)
tests
├── admin
│ └── Login.spec.ts
└── front
├── mobile
│ └── Login.spec.ts
└── pc
└── Login.spec.ts
tests 내부도 src와 같이 depth를 환경 - 서비스 - 스킨
으로 잡았다.
테스트 코드 작성은 보통 아래와 같은 구조를 따른다.
/**
* @Title ( 테스트 제목 )
* @Level ( 테스트 우선순위 )
* @Given
* ( 테스트 사전 조건)
* @When
* ( 테스트 수행 절차 )
* @Then
* ( 테스트 기대 결과 )
*/
test("[FRONT][PC] Login_0001: 로그인_성공", async ({ page }) => {
// given
let mainPage = new MainPage(page)
let header = new Header(page)
let loginPage = new LoginPage(page)
const fixture = Fixture.get("Member", "commonUser")
// when
await mainPage.open()
await page.click(header.locators.aLogin)
await page.fill(loginPage.locators.inputId, fixture.id)
await page.fill(loginPage.locators.inputPw, fixture.pw)
await page.click(loginPage.locators.buttonLogin)
await page.waitForNavigation({ waitUntil: "load", timeout: 10000 })
// then
const expected = Skin.SKIN == "(skinA)" ? "LOGOUT" : "로그아웃"
expect(await page.textContent(header.locators.aLogout)).toBe(expected)
})
테스트 코드를 보면 아마 아래와 같이 구분한 이유가 더 명확하게 보이지 않을까 싶다.
├── login
├── ILoginLocators.ts
├── LoginPage.ts
├── skinA
│ └── LoginLocators.ts
└── skinB
└── LoginLocators.ts
스킨별 Locators 클래스는 같은 Locators 인터페이스를 바라본다.
const { LoginLocators } = require ("./" + Skin.SKIN + "/LoginLocators")
export class LoginPage extends PcPageCommon {
readonly locators: ILoginLocators
constructor(page: Page) {
super(page, PagePath.LOGIN_PAGE)
this.locators = new LoginLocators()
}
}
구체적인 Locators 값은 각 스킨별로 구분되어있기 때문에
테스트가 돌아가는 skin env 값에 따라 자동으로 적절한 skin의 Locators를 가져오게 된다.
위 구조를 바탕으로 전반적인 테스트 케이스 개발이 가능해졌다.
내가 고민해서 만든 구조가 정답인지는 모르겠지만.. 여러번 갈아엎는 과정에서 비효율적인 구조를 정말 많이 버렸기 때문에 평균 이상의 역할은 하고 있다고 생각한다.
테스트 코드 작성이 완료되었다면, 이제는 실제 배포 프로세스에 E2E 테스트를 녹일 차례다.
이건 다음 포스팅에서 이어서 진행해야겠다 !