테스트 생성기는 연습 문제의 테스트를 자동으로 만들어 주는 트랙별 소프트웨어예요. 연습 문제의 JSON 테스트 케이스를 해당 트랙 언어의 테스트로 변환하는 방식으로 동작해요.
테스트 생성기가 있으면 다음과 같은 장점이 있어요:
일반적으로 테스트 생성기는 다음 두 가지 경우에 실행해요:
새 연습 문제에 테스트 생성기를 도입하면 그 문제의 테스트 파일을 생성할 수 있어요. 테스트 생성기 자체가 이미 구현되어 있다면, 새 연습 문제의 테스트를 생성하는 일은 처음부터 직접 작성하는 것보다 (훨씬) 적은 작업이에요.
연습 문제에 테스트 생성기가 있으면, 다시 실행해서 그 문제를 최신 표준 데이터와 갱신하거나 동기화할 수 있어요. 갱신해야 할 문제 있는 테스트 케이스가 있는지, 또는 새로 포함하고 싶은 테스트가 있는지 확인하기 위해 주기적으로 실행하는 걸 권장해요.
연습 문제용 테스트 생성기를 구현할 때 시작점은 두 가지예요:
기존 테스트가 있다면, 생성기가 만들어 내는 테스트가 기존 풀이를 망가뜨리지 않도록 테스트 생성기를 구현해야 해요.
대체로 테스트 파일은 다음 두 가지 방식 중 하나로 생성해요:
코드 기반 방식은 테스트 생성기 코드가 꽤 복잡해지는 반면, 템플릿 기반 방식은 더 단순하다는 걸 알게 되었어요.
권장하는 흐름은 다음과 같아요:
tests.toml 파일에서 include = false로 표시된 테스트 케이스를 제외해요이 구조의 핵심 장점은 각 연습 문제가 자기만의 템플릿을 가진다는 점이에요. 덕분에:
테스트 생성기를 설계할 때는 다음을 목표로 해요:
테스트 생성기는 보통 (대부분) 트랙 언어로 작성해요.
다른 언어를 사용해도 괜찮지만, 언어가 하나 늘어날 때마다 트랙을 유지하거나 기여하기가 더 어려워져요. 그래서 가능하면 트랙 언어를 사용하는 걸 권장해요. 유지와 기여가 더 쉬워지니까요.
트랙에 코드를 서식화하는 도구가 있다면, 템플릿을 렌더링한 뒤에 후처리 단계로 실행하는 걸 고려해 봐요.
테스트 생성기가 다루는 핵심 데이터는 연습 문제의 canonical-data.json 파일이에요.
이 파일은 exercism/problem-specifications 저장소에 정의되어 있고, 이 저장소는 많은 Exercism 연습 문제가 공유하는 메타데이터를 정의해요.
모든 연습 문제에 canonical-data.json 파일이 있는 건 아니에요!
그런 경우에는 테스트 생성기가 다룰 데이터가 없으니, 테스트를 직접 만들어야 해요.
표준 데이터는 JSON 객체로 정의해요.
이 객체에는 테스트 케이스를 담은 "cases" 필드가 있어요.
이 테스트 케이스는 (보통) 트랙의 테스트와 일대일로 대응해요.
각 테스트 케이스에는 여러 속성이 있는데, 그중 description, property, input, expected가 가장 중요해요. 다음은 leap 연습 문제의 canonical-data.json 파일의 (일부) 예시예요:
{
"exercise": "leap",
"cases": [
{
"uuid": "6466b30d-519c-438e-935d-388224ab5223",
"description": "year not divisible by 4 in common year",
"property": "leapYear",
"input": {
"year": 2015
},
"expected": false
},
{
"uuid": "4fe9b84c-8e65-489e-970b-856d60b8b78e",
"description": "year divisible by 4, not divisible by 100 in leap year",
"property": "leapYear",
"input": {
"year": 1996
},
"expected": true
}
]
}
테스트 생성기의 주된 역할은 이 JSON 데이터를 트랙에 맞는 테스트로 변환하는 거예요. 위 JSON은 다음과 같이 Nim 테스트 코드로 변환할 수 있어요:
import unittest
import leap
suite "Leap":
test "year not divisible by 4 in common year":
check isLeapYear(2015) == false
test "year divisible by 4, not divisible by 100 in leap year":
check isLeapYear(1996) == true
canonical-data.json 파일의 구조는 잘 문서화되어 있고, JSON 스키마 정의도 있어요.
일부 연습 문제는 표준 데이터에 중첩을 사용해요.
즉 cases 배열의 각 요소는 다음 둘 중 하나예요:
요소의 종류는 특정 종류에만 있는 필드가 있는지 확인해서 구분할 수 있어요.
아마 가장 좋은 방법은 테스트 케이스 그룹에만 있는 "cases" 키를 사용하는 거예요.
다음은 중첩된 테스트 케이스의 예시예요:
{
"cases": [
{
"uuid": "e9c93a78-c536-4750-a336-94583d23fafa",
"description": "data is retained",
"property": "data",
"input": {
"treeData": ["4"]
},
"expected": {
"data": "4",
"left": null,
"right": null
}
},
{
"description": "insert data at proper node",
"cases": [
{
"uuid": "7a95c9e8-69f6-476a-b0c4-4170cb3f7c91",
"description": "smaller number at left node",
"property": "data",
"input": {
"treeData": ["4", "2"]
},
"expected": {
"data": "4",
"left": {
"data": "2",
"left": null,
"right": null
},
"right": null
}
}
]
}
]
}
트랙이 테스트 그룹을 지원하지 않는다면 다음을 해야 해요:
cases 계층을 순회하거나 평탄화해서 가장 안쪽의 (리프) 테스트 케이스만 남겨요테스트 케이스의 input과 expected 키에 담기는 내용은 아주 다양해요.
대부분은 (숫자, 불리언, 문자열 같은) 스칼라 값이거나 단순한 객체예요.
하지만 가끔은 의사 코드의 람다, 학습자 코드에 수행할 연산 목록 등 약간의 전처리가 필요한 더 복잡한 값도 나와요.
테스트 케이스에는 선택적인 scenarios 필드가 있어요.
테스트 생성기는 이 필드로 특정 테스트 케이스를 특별 취급할 수 있어요.
가장 흔한 활용은 특정 유형의 테스트를 무시하는 건데, 예를 들어 트랙 언어가 유니코드를 지원하지 않을 때 "unicode" 시나리오가 붙은 테스트를 무시할 수 있어요.
시나리오 전체 목록은 여기에서 볼 수 있어요.
canonical-data.json 파일을 읽는 방법은 몇 가지가 있어요:
problem-specifications 저장소에서 직접 가져와요 (예: https://raw.githubusercontent.com/exercism/problem-specifications/main/exercises/leap/canonical-data.json).problem-specifications 저장소를 트랙 저장소에 Git 서브모듈로 추가해요.configlet 캐시에서 읽어요.
위치는 사용자의 시스템에 따라 다르지만, configlet info -o -v d | head -1 | cut -d " " -f 5를 사용하면 위치를 프로그래밍 방식으로 알아낼 수 있어요.트랙에만 해당하는 추가 테스트 케이스(표준 데이터에는 없는)를 넣고 싶다면, additional-test-cases.json 파일을 만들어서 테스트 생성기가 템플릿에 넘겨 렌더링하기 전에 canonical-data.json 파일과 병합하게 하는 방법이 있어요.
사용할 템플릿 엔진은 트랙마다 다를 거예요. 이상적으로는 템플릿이 최대한 단순하기를 바랄 테니, 코드 중복 같은 건 걱정하지 않아도 돼요.
템플릿은 테스트 생성기로부터 데이터를 받고, 생성기는 그 데이터를 순회하면서 템플릿을 렌더링해요.
템플릿을 단순하게 유지하려면, 테스트 생성기 쪽에서 약간의 전처리를 하거나, 템플릿이 허용하는 확장 메커니즘(예: "필터")을 정의하는 게 도움이 될 수 있어요.
configlet은 트랙 유지보수의 핵심 도구로, 다음과 같이 사용할 수 있어요:
bin/configlet create --practice-exercise <slug>를 실행해요tests.toml 파일을 동기화해요: bin/configlet sync --tests --update --exercise <slug>를 실행해요그래서 configlet은 테스트 생성기와 함께 쓰면 아주 강력한 워크플로를 만들 수 있는 좋은 도구예요.
테스트 생성기를 쓰기 쉬우면서도 강력하게 만들고 싶을 거예요. 그러려면 스크립트 파일을 하나 이상 만드는 걸 권장해요.
트랙에 가장 잘 맞는 스크립트 파일 형식을 자유롭게 고르면 돼요. 셸 스크립트와 PowerShell 스크립트는 둘 다 잘 동작하는 흔한 선택지예요.
다음은 configlet과 테스트 생성기를 조합해서 새 연습 문제의 뼈대를 빠르게 만드는 셸 스크립트 예시예요:
bin/fetch-configlet
bin/configlet create --practice-exercise <slug>
path/to/test-generator <slug>
테스트 생성기를 만들기 시작하기 전에, 다른 트랙이 어떻게 구현했는지 감을 잡을 수 있도록 기존 테스트 생성기 몇 개를 살펴보는 걸 추천해요:
질문이 있으면 포럼이 가장 좋은 곳이에요. Rust 테스트 생성기와 JavaScript 테스트 생성기에 관한 포럼 논의도 도움이 될 거예요.
테스트 생성기는 최소 기능 제품부터 시작해서 점진적으로 만드는 걸 권장해요.
최소한의 버전은 연습 문제의 canonical-data.json을 읽어서 그 데이터를 그대로 템플릿에 넘겨주기만 하면 돼요.
먼저 leap처럼 단순한 연습 문제 하나에 집중해요.
그게 동작한 다음에야 연습 문제를 점차 늘려가요.
그리고 테스트 생성기는 최대한 단순하게 유지하려고 해요.
이상적으로는 기여자가 테스트 생성기의 내부 동작을 이해하지 않고도 기존 템플릿을 붙여 넣거나 수정할 수 있어야 해요.
테스트 생성기를 사용하거나 기여하는 방법은 트랙마다 달라요.
트랙의 README.md, CONTRIBUTING.md 또는 테스트 생성기 코드 디렉터리에서 안내를 찾아봐요.