📖 개요 및 이 글에서 다룰 것
SAPUI5/OpenUI5 애플리케이션 개발에서 OData 호출, Promise 체인, setTimeout 같은 비동기 로직은 QUnit 단위 테스트의 가장 흔한 함정입니다. 동기 테스트처럼 코드를 작성하면 assert가 실행되기 전에 테스트 함수가 종료되어 잘못된 통과 결과를 받게 됩니다. 이 글은 QUnit이 제공하는 assert.async()의 done() 콜백 패턴을 중심으로, Promise 반환 방식과 testTimeout 설정까지 세 가지 비동기 처리 방법을 비교합니다.
이 글에서 다룰 것 체크리스트
assert.async()로 done 핸들을 획득하고 완료 시점을 명시- Promise를 그대로
return하여 자동 완료 감지를 활용QUnit.config.testTimeout으로 무한 대기 방지- 다중 done 호출(
assert.async(2)) 및 에러 처리 패턴- UI5 카르마(karma-ui5) 환경에서의 실전 적용
📚 이 글을 보기 전에
QUnit 기본 문법(QUnit.module, QUnit.test, assert.ok/strictEqual)을 알고 있어야 합니다. 또한 JavaScript Promise(then/catch/finally)와 ES6 화살표 함수, UI5 모듈 시스템(sap.ui.define)에 대한 기초 이해가 필요합니다. UI5 테스트 러너인 testsuite.qunit.html 구조와 opaTests.qunit.js 와의 차이를 알면 더 빠르게 따라올 수 있습니다.
🔧 환경 / 버전 / 준비물
- SAPUI5/OpenUI5: 1.108 LTS 이상 권장 (1.71 LTS도 동일 패턴 동작)
- QUnit: UI5 번들에 포함된 QUnit 2.x (별도 설치 불필요)
- Node.js: 18.x LTS 이상, npm 9 이상
- UI5 Tooling:
@ui5/cli3.x, 로컬 실행은ui5 serve - karma-ui5: CI 환경 자동 실행용 (선택)
- 브라우저: Chromium 계열 권장 (Headless 테스트 호환)
프로젝트 폴더 구조는 일반적으로 webapp/test/unit/ 하위에 *.qunit.js 파일을 두고, unitTests.qunit.html에서 모듈을 import 합니다. SAP Business Application Studio(BAS) 사용자는 Fiori freestyle 템플릿이 이 구조를 기본 생성합니다.
💡 핵심 개념
QUnit 테스트 함수는 기본적으로 동기 실행으로 가정합니다. QUnit.test()에 전달된 콜백이 끝나는 순간 QUnit은 "이 테스트가 종료됐다"고 판단하고 다음 테스트로 넘어갑니다. 그래서 setTimeout이나 Promise 내부의 assert는 이미 종료된 테스트 컨텍스트에서 실행되어 결과가 누락되거나 다음 테스트를 오염시킵니다.
비유: 식당의 영수증
QUnit을 식당 주방이라고 상상하면, 동기 테스트는 "주문 → 조리 → 영수증 발행"이 한 호흡에 끝나는 패스트푸드입니다. 반면 비동기 테스트는 코스 요리에 가까워서, 메인 디시(Promise)가 나오기 전에 영수증을 끊으면 안 됩니다. assert.async()는 "아직 음식 다 안 나왔으니 영수증 잠깐 보류해 주세요"라고 말하는 대기표이고, done()은 "이제 다 먹었으니 계산해 주세요"라는 호출 벨입니다.
세 가지 패턴 비교
| 패턴 | 완료 신호 | 추천 상황 |
|---|---|---|
| done() 콜백 | 명시적 done() 호출 | 여러 비동기 흐름이 섞인 경우, 콜백 기반 API |
| Promise return | 반환 Promise가 settle | 단일 Promise 체인, async/await 친화 |
| testTimeout | 지정 ms 초과 시 실패 | 전역 안전망, 무한 대기 방지 |
assert.async()는 호출할 때마다 새로운 done 함수를 반환합니다. 인자에 숫자를 넘기면(assert.async(3)) 그 횟수만큼 호출되어야 테스트가 완료된 것으로 간주됩니다. 이는 병렬 비동기 작업(예: 동시 OData 호출 2개)에 유용합니다.
💻 실전 코드 3단계
1단계: 기본 done() 콜백 패턴
OData 모델이나 fetch API처럼 Promise를 반환하는 서비스 호출을 테스트하는 가장 표준적인 방법입니다. assert.async()로 done 핸들을 받고, then과 catch 양쪽 분기에서 모두 done()을 호출하는 것이 핵심입니다.
sap.ui.define([
"myapp/service/MyService"
], function(MyService) {
"use strict";
QUnit.module("MyService - 기본 비동기");
QUnit.test("비동기 서비스 호출", function(assert) {
var done = assert.async();
MyService.fetchData("001").then(function(data) {
assert.ok(data, "데이터 반환 확인");
assert.strictEqual(data.id, "001", "ID 일치");
done();
}).catch(function(err) {
assert.ok(false, "에러: " + err.message);
done();
});
});
});
주의 포인트: catch에서도 done()을 빠뜨리면 실패 시 테스트가 무한 대기에 빠집니다. 또한 assert.ok(false, ...) 형태로 강제 실패 메시지를 남기면 디버깅 로그가 유용해집니다.
2단계: Promise return으로 간결화 + 에러 로깅
QUnit 2.x는 테스트 콜백이 thenable을 반환하면 자동으로 비동기 모드로 전환하고, Promise가 resolve/reject 될 때까지 대기합니다. done()을 호출할 필요가 없어 코드가 한결 깔끔해집니다.
sap.ui.define([
"myapp/service/MyService",
"sap/base/Log"
], function(MyService, Log) {
"use strict";
QUnit.module("MyService - Promise 반환", {
beforeEach: function() {
Log.setLevel(Log.Level.DEBUG, "myapp.test");
}
});
QUnit.test("Promise 비동기 테스트", function(assert) {
return MyService.fetchData("002")
.then(function(data) {
assert.ok(data, "데이터 반환 확인");
assert.strictEqual(data.status, "ok");
})
.catch(function(err) {
Log.error("fetchData 실패", err, "myapp.test");
throw err; // QUnit이 실패로 인식하도록 재던지기
});
});
QUnit.test("async/await 스타일", async function(assert) {
try {
var data = await MyService.fetchData("003");
assert.deepEqual(data.items.length, 5, "5건 반환");
} catch (e) {
assert.ok(false, "예외 발생: " + e.message);
}
});
});
UI5의 sap/base/Log 모듈로 카테고리별 로그를 남기면 Karma reporter에서 실패 원인을 추적하기 좋습니다. throw로 에러를 다시 던지지 않으면 catch에서 삼켜져서 테스트가 거짓 통과할 수 있으니 주의하세요.
3단계: testTimeout + 다중 done + Mock 격리
프로덕션 테스트에서는 무한 대기를 막기 위한 전역 타임아웃, 외부 의존성을 끊기 위한 sinon 스텁, 병렬 호출 검증을 위한 다중 done이 함께 쓰입니다.
sap.ui.define([
"myapp/service/MyService",
"sap/ui/thirdparty/sinon",
"sap/ui/thirdparty/sinon-qunit"
], function(MyService, sinon) {
"use strict";
// 모든 비동기 테스트에 5초 타임아웃 적용
QUnit.config.testTimeout = 5000;
QUnit.module("MyService - 프로덕션 패턴", {
beforeEach: function() {
this.oSandbox = sinon.sandbox.create();
},
afterEach: function() {
this.oSandbox.restore();
}
});
QUnit.test("타임아웃 적용 테스트", function(assert) {
var done = assert.async();
this.oSandbox.stub(MyService, "slowFetch").resolves({ ok: true });
MyService.slowFetch().then(function(data) {
assert.ok(data, "응답 확인");
done();
});
});
QUnit.test("병렬 호출 - 다중 done", function(assert) {
var done = assert.async(2); // 2번 호출돼야 완료
MyService.fetchData("A").then(function(r) {
assert.strictEqual(r.id, "A");
done();
});
MyService.fetchData("B").then(function(r) {
assert.strictEqual(r.id, "B");
done();
});
});
QUnit.test("개별 타임아웃 오버라이드", function(assert) {
var iOriginal = QUnit.config.testTimeout;
QUnit.config.testTimeout = 10000; // 이 케이스만 10초
var done = assert.async();
MyService.verySlow().finally(function() {
QUnit.config.testTimeout = iOriginal;
done();
});
});
});
보안/안정성 관점: 실제 백엔드 호출을 단위 테스트에 포함하면 환경 의존도가 높아지고 CI가 불안정해집니다. sinon.sandbox로 모든 외부 호출을 스텁 처리하고, afterEach에서 restore()를 호출해 테스트 간 격리를 보장하는 패턴이 일반적으로 권장됩니다.
⚠️ 흔한 실수 / 트러블슈팅
비동기 QUnit 테스트에서 가장 빈번한 문제는 "테스트는 통과했는데 실제로는 assert가 실행되지 않은" 거짓 통과 케이스입니다. 아래 FAQ를 통해 원인을 점검하세요.
FAQ 1. done()을 catch에서 빼먹어 테스트가 hang 됩니다
증상: 하나의 테스트에서 멈춰 다음 테스트로 넘어가지 않습니다. 원인은 reject 분기에서 done()이 호출되지 않은 것입니다. finally에 done()을 두면 양쪽 분기에서 한 번만 호출되어 안전합니다. 또는 Promise return 패턴으로 전환하세요.
FAQ 2. assert.expect()와 다르게 assert가 더 적게 실행됩니다
assert.expect(2)처럼 호출하면 정확히 2개의 assert가 실행돼야 통과합니다. 비동기 분기 중 하나가 누락되면 "Expected 2 assertions, but 1 were run"이 출력됩니다. 비동기 테스트에서는 assert.expect()를 명시하는 습관이 거짓 통과를 막아 줍니다.
FAQ 3. testTimeout을 설정해도 무한 대기가 풀리지 않습니다
QUnit.config.testTimeout은 테스트 파일이 로드되기 전에 설정되어야 합니다. 일반적으로 unitTests.qunit.js 부트스트랩 상단이나 QUnit.begin 콜백에서 지정합니다. 개별 테스트 내부에서 변경할 수도 있지만, 그 경우 테스트 종료 시 원래 값으로 되돌리세요.
FAQ 4. Promise return과 done()을 같이 쓰면?
두 패턴을 동시에 사용하면 QUnit이 어느 시점에 완료를 인식할지 모호해져 예상치 못한 동작을 일으킬 수 있습니다. 둘 중 하나만 선택하세요. 단일 Promise 체인은 return, 복잡한 콜백 합성은 done() 방식이 자연스럽습니다.
🚀 다음 단계 / 관련 주제
QUnit 비동기 패턴이 익숙해졌다면 다음 주제로 확장해 보세요.
- OPA5 통합 테스트: UI 인터랙션을 시뮬레이션하는 SAPUI5 전용 프레임워크
- sinon-chai / fake-server: OData v2/v4 응답 모킹
- karma-ui5: CI 파이프라인에서 헤드리스 브라우저로 자동 실행
- Code Coverage:
karma-coverage와 BlanketJS로 커버리지 리포트 수집 - Wing 테스트 격리:
QUnit.module단위 sandbox 패턴 심화
댓글 0
아직 댓글이 없습니다.