우테코 프리코스를 진행하면서, 문제를 잘 푸는 방법이 아닌 함께 일하기 좋은 개발자가 되는 방법을 배웠다고 생각한다. 시작 전엔 난도가 높은 문제를 얼마나 빨리, 얼마나 정교하게 풀 수 있느냐에만 시선이 갔다. 그런데 실제로는 기능 구현 그 자체보다 요구사항을 엄격히 해석하고 지키는 태도, 좋은 커밋 메시지와 코딩 컨벤션, 그리고 협업을 전제한 듯한 개발 습관을 끊임없이 요구받았다. 그 과정에서 우테코가 말하는 성장은 기술 스택 뿐만 아니라 규칙을 존중하는 태도에서 비롯된다는 것을 체감했다.
우테코에서 말했던 모든 부분들을 스스로 다시 곱씹어봤다. “왜 README는 살아 있는 문서여야 할까?”, “왜 메서드는 한 가지 일만 해야 할까?”, “테스트는 왜 미루지 말아야 할까?”, “객체를 객체답게 쓴다는 건 무슨 의미고, 왜 그렇게 해야 하는걸까?”
이 질문들에 답하기 위해 많은 시간을 들여 고민했고, 그에 맞게 내 코드도 조금씩 달라졌다. README는 기능을 추가할 때마다 마치 살아 있는 것처럼 업데이트되는 소개서가 되었고, 메서드를 한 책임으로 쪼개니 변경에 강한 구조가 보였다. 테스트는 프로그램의 안전망이자 그 자체로 명세가 되었고, 객체 지향적으로 설계하다보니 도메인이 선명해져서 다른 개발자와의 의사소통이 원활해지는 느낌을 받았다.
물론 진행하면서 결코 쉽지 않았다. 혼자 부딪히는 시간이 길었고, 다른 멤버들의 화려한 이론과 복잡한 코드 앞에서 주눅 들기도 했다. 하지만 공통 피드백을 받고, 코드 리뷰에서 다양한 의견을 들으며, 다음 과제에 하나씩 반영하는 과정을 거치자 내 코드가 눈에 띄게 개선되는 것이 보였다. 몰입은 그 순간 순간 생긴 것 같다. 작게 개선하고, 피드백 받고 다시 개선하는 과정이 쌓이면 쌓일수록 도전은 막막함이 아니라 성장의 밑거름이자 더 나아가 즐거움이 되었던 것 같다.
그래서 나는 이번 오픈 미션에서 오픈 소스 기여를 목표로 삼았다. 사실 그동안 지레 겁 먹어서 오픈 소스에 기여해 볼 엄두도 내지 못 했다. 하지만 돌이켜보면, 우테코에서 했던 요구사항을 해석하고, 규칙을 지키고, 리뷰로 더 나은 합의를 찾아가는 과정이야말로 오픈 소스에서는 일상이 아닐까 하는 생각이 들었다. 전 세계의 개발자들이 문제를 나누고, 대안을 제시하고, 커뮤니케이션의 질을 높이며, 작은 변화를 지속적으로 합치는 문화. 이번 오픈 미션에서는 그 한가운데로 들어가 보고 싶다는 생각을 했다.
물론 내 의견이 반영되지 않을 수도 있고, 처음엔 사소한 문서 수정부터 시작해야 할지도 모른다. 그럼에도 불구하고, 불확실성 속에서 시도하고 배우는 경험 자체가 몰입의 장이 될 거라 믿는다. 리뷰 코멘트를 통해 내가 놓친 전제와 더 좋은 관례를 배우고, 프로젝트의 기여 가이드를 직접 몸으로 익히면서 다양한 커뮤니케이션 규칙을 준수하면 한층 더 발전한 나의 모습을 발견할 수 있을 것이라고 생각했다. 이런 부분들이 바로 우테코가 강조하던 새로운 시도를 멈추지 않는 태도라고 생각했고, 내가 개발자로서 계속 가지고 가고 싶은 습관이기도 하다.
따라서 이번 미션의 목표는 간단하다. 우테코에서 겪은 경험을 토대로 혼자 성장하는 개발자에서 한 걸음 더 나아가, 함께 더 잘 성장하는 법을 실천하는 개발자가 되는 것이다. 그 부분에서 오픈 소스 기여는 그 목표를 가장 현실적이고 효율적으로 훈련할 수 있는 방법이라고 생각했다. 작은 PR 하나가 해당 프로젝트의 방식을 배우는 첫걸음이 되고, 이슈에 달린 짧은 코멘트 하나가 내 시야를 넓혀줄 것이라고 믿는다. 나는 그 과정을 통해 다시 배우고, 명료하게 의사소통할 수 있는 개발자가 되고 싶다. 그게 내가 이번 오픈 미션에서 선택한 도전이며, 몰입이고, 다음 단계의 새로운 시도다.
그래서 장시간의 고민 끝에 이번 오픈 미션의 무대로 TEAMMATES라는 오픈소스를 선택했다. 내가 개발자가 된 이유 중 하나로 사람들이 겪는 문제점을 해결해주는 것이라면, TEAMMATES는 바로 그 접점에 있다고 생각했다. 학생들의 동료 평가 및 기타 피드백 경로를 구조화함으로써 서로 다양한 의견을 주고 받고, 교육자의 멘토링을 받으면서 서로 문제를 인식하고 해결해나가는 이 서비스에 기여하는 일은, 내가 우테코에서 배운 원칙을 실제 문제 해결로 연결하는 가장 직접적인 방법이라고 생각했다. 다시 말해, 내가 개발자가 되려 한 동기와 우테코가 강조한 태도를 한 프로젝트 안에서 동시에 검증할 수 있는 방법이 TEAMMATES라고 생각했다. 내 작은 기여가 곧바로 학습자와 교육자의 경험을 개선하고, 그 과정 자체가 내가 지키고 싶은 개발 문화를 코드로 증명해 줄 것이라고 생각한다.
TEAMMATES의 주요 기능을 살펴보고 아래와 같이 정리해봤다.
<참고 링크>
TEAMMATES - Online Peer Feedback/Evaluation System for Student Team Projects












TEAMMATES의 주요 기능과 실행 플로우를 전체적으로 살펴보고 작업 환경을 세팅해줬다. 그 후, 해결해야 할 여러 이슈 중에 내가 맡고 싶은 이슈를 선택해야 했는데, TEAMMATES의 기여 가이드라인에서 말하길, 처음 기여하는 사람들은 good first issue 태그가 붙은 이슈를 선택하는 것을 권장하고 있었다.

그래서 찾아보던 중, “Unit Test” 라는 문구가 한눈에 들어왔다. 단위 테스트 작성을 통해 특정 기능이 어떻게 동작하는지 쉽게 파악할 수 있고, 향후 해당 기능을 담당할 개발자들이 그 기능에 해당하는 테스트 코드를 유심히 볼 것이기 때문에 우테코에서 배운 테스트 코드를 작성하는 이유와 코딩 컨벤션을 한번에 실무에 적용할 수 있는 좋은 이슈인 것 같아 선정하였다.
이슈 링크 -> Refactoring for Unit Test Access Controls
#13304 이슈에 들어가보니, 내가 이해한 내용은 대략 아래와 같았다.
이번 릴리즈에서 PostgreSQL을 도입하면서 많은 단위 테스트들이 ui/webapi 폴더에서 sqlui/webapi 폴더로 마이그레이션 되었는데, 이때 접근 제어 테스트 메서드들의 코드 스타일이 뒤죽박죽된 것 같다. 가장 최근 PR에서 모든 기능에 대한 테스트 메서드들을 효율적으로 조작할 수 있도록 상위 수준의 메서드들을 sqlui/webapi/BaseActionTest.java에 작성해 놓았고, 여기에 있는 메서드들을 우선적으로 적용해 여러 기능에 대한 테스트 코드를 리팩토링 해야 하는 것이다.
<최근 PR>
PR 링크 -> [#12048] Abstract Access Controls to BaseActionTest by InfinityTwo
첨부된 진행 상황에 대한 구글 시트를 참고해서 다른 기여자들이 선점하지 않은 단위 테스트들 중에서 선택해야 했었는데, 생각보다 정말 많은 기여자들이 작업을 진행하고 있었다. 거의 모든 기능들이 작업 중에 있어서 선택권이 그리 많지는 않았지만, 그 중에서 2개를 선택하였다. 테스트를 간단하게 말하자면,
GetStudentActionTest.java
GetStudentsActionTest.java
GetStudentActionTest와 비슷하게 세밀한 접근 제어를 검증하는 테스트다.2개의 테스트 모두 학생(들) 정보를 조회하고, 그 정보를 누가 볼 수 있는지에 대한 권한을 검증하는 테스트다. 위에서 요약했던 주요 기능 중 Fine-grain access control(접근 권한 세분화)과 Powerful search(강력한 검색)에 해당하는 것 같다. 내가 감당해야 할 부분이 얼마나 무거울지 빠르게 판단하고, 다른 사람들이 선점하기 전에 빠르게 코멘트를 달았다.

InfinityTwo님이 작업하신 BaseActionTest.java 파일을 토대로 sqlui/webapi 폴더에 있는 GetStudentActionTest.java와 GetStudentsActionTest.java 파일을 리팩토링 해야 한다.
그러기 위해 먼저 내가 참고해야 할 BaseActionTest.java 파일의 내용을 살펴봤다. 각 단위 테스트에서 누가 접근 가능할 수 있는지를 역할별로 표준화된 방식으로 검증하게 해 주는 공용 메서드로 이 메서드들을 사용하면 동일한 패턴을 반복 작성하지 않아도 되는 것 같다.
// Admins
void verifyOnlyAdminsCanAccess(String... params) {
verifyAdminsCanAccess(params);
verifyInstructorsCannotAccess(params);
verifyStudentsCannotAccess(params);
verifyUnregisteredCannotAccess(params);
verifyWithoutLoginCannotAccess(params);
}
// Instructors
void verifyOnlyInstructorsCanAccess(Course currentCourse, String... params) {
verifyAnyInstructorCanAccess(currentCourse, params);
verifyStudentsCannotAccess(params);
verifyUnregisteredCannotAccess(params);
verifyWithoutLoginCannotAccess(params);
}
void verifyOnlyInstructorsOfTheSameCourseCanAccess(Course currentCourse, String... params) {
Instructor sameCourseInstructor = getTypicalInstructor();
sameCourseInstructor.setCourse(currentCourse);
verifyInstructorsOfTheSameCourseCanAccess(currentCourse, params);
verifyAccessibleForAdminsToMasqueradeAsInstructor(sameCourseInstructor, params);
verifyInstructorsOfOtherCoursesCannotAccess(params);
verifyStudentsCannotAccess(params);
verifyUnregisteredCannotAccess(params);
verifyWithoutLoginCannotAccess(params);
}
void verifyOnlyInstructorsOfTheSameCourseWithCorrectCoursePrivilegeCanAccess(
Course thisCourse, String privilege, String... submissionParams) {
verifyAccessibleWithCorrectSameCoursePrivilege(thisCourse, privilege, submissionParams);
verifyInaccessibleWithoutCorrectSameCoursePrivilege(thisCourse, privilege, submissionParams);
verifyStudentsCannotAccess(submissionParams);
verifyUnregisteredCannotAccess(submissionParams);
verifyWithoutLoginCannotAccess(submissionParams);
}
void verifyOnlyInstructorsOfTheSameCourseWithCorrectCoursePrivilegeCanAccess(
Course thisCourse, InstructorPrivileges privilege, String... submissionParams) {
verifyAccessibleWithCorrectSameCoursePrivilege(thisCourse, privilege, submissionParams);
verifyInaccessibleWithoutCorrectSameCoursePrivilege(thisCourse, privilege, submissionParams);
verifyStudentsCannotAccess(submissionParams);
verifyUnregisteredCannotAccess(submissionParams);
verifyWithoutLoginCannotAccess(submissionParams);
}
// Students
void verifyOnlyStudentsCanAccess(String... params) {
verifyStudentsCanAccess(params);
verifyUnregisteredCannotAccess(params);
verifyWithoutLoginCannotAccess(params);
}
// Unregistered
void verifyOnlyLoggedInUsersCanAccess(String... params) {
verifyUnregisteredCanAccess(params);
verifyWithoutLoginCannotAccess(params);
}
void verifyAnyLoggedInUserCanAccess(String... params) {
verifyAdminsCanAccess(params);
verifyInstructorsCanAccess(getTypicalCourse(), params);
verifyStudentsCanAccess(params);
verifyUnregisteredCanAccess(params);
verifyWithoutLoginCannotAccess(params);
}
// No Login
void verifyOnlyLoggedOutUsersCanAccess(String... params) {
verifyWithoutLoginCanAccess(params);
}
// All or none
void verifyAnyUserCanAccess(String... params) {
verifyAdminsCanAccess(params);
verifyMaintainersCanAccess(params);
verifyInstructorsCanAccess(getTypicalCourse(), params);
verifyStudentsCanAccess(params);
verifyUnregisteredCanAccess(params);
verifyWithoutLoginCanAccess(params);
}
void verifyNoUsersCanAccess(String... params) {
verifyAdminsCannotAccess(params);
verifyInstructorsCannotAccess(params);
verifyStudentsCannotAccess(params);
verifyUnregisteredCannotAccess(params);
verifyWithoutLoginCannotAccess(params);
}
적혀 있는 주석을 읽어 보면,
/* Access control methods */
/*
* High-level access control tests: Test an action's access control across all
* user types.
*
* - Prefer high-level tests over mid-level tests when possible.
* - Follow this user type order when adding new access control tests:
* Admin → Maintainer → Instructor → Student → Unregistered → No login
* - An 'Only' can access test means all other types to the right cannot access except maintainers,
* which should be tested separately as they are a separate unrelated entity.
*/
가능한 한 위의 제시된 High-level 테스트 메서드로 한 번에 여러 사용자 유형을 묶어서 검증하고, 필요할 때만 Mid-level 테스트 메서드를 사용하라는 의미 같다. 사용자의 접근 권한을 높은 순으로 나열하면, Admin → Maintainer → Instructor → Student → Unregistered → No login 순서고, 위와 같이 “Only” 키워드가 붙은 테스트 메서드는 명시되어 있는 접근 주체보다 권한이 낮은 사용자의 접근은 전혀 허용되지 않는다. 하지만, Maintainer는 Admin과는 독립된 역할이라서, “Only” 키워드에 자동 포함되지 않는다. 그래서 Maintainer는 따로 허용 가능한지, 불가능한지를 추가로 확인해야 한다.
High-level 테스트 메서드 아래에 정의된 Mid-level 테스트 메서드다.
// Admins
void verifyAdminsCanAccess(String... params) {
loginAsAdmin();
verifyCanAccess(params);
logoutUser();
}
void verifyAdminsCannotAccess(String... params) {
loginAsAdmin();
verifyCannotAccess(params);
logoutUser();
}
void verifyAccessibleForAdminsToMasqueradeAsInstructor(Instructor instructor, String... params) {
loginAsAdminAndMasqueradeAsInstructor(instructor, true, params);
}
void verifyInaccessibleForAdminsToMasqueradeAsInstructor(Instructor instructor, String... params) {
loginAsAdminAndMasqueradeAsInstructor(instructor, false, params);
}
// Maintainers
void verifyMaintainersCanAccess(String... params) {
loginAsMaintainer();
verifyCanAccess(params);
logoutUser();
}
void verifyMaintainersCannotAccess(String... params) {
loginAsMaintainer();
verifyCannotAccess(params);
logoutUser();
}
// Instructors
void verifyInstructorsCanAccess(Course thisCourse, String... params) {
// Looser version of verifyInstructorsOfTheSameCourseCanAccess
loginAsInstructorOfTheSameCourse(thisCourse);
verifyCanAccess(params);
logoutUser();
// Looser version of verifyInstructorsOfOtherCoursesCanAccess
loginAsInstructorOfOtherCourse();
verifyCanAccess(params);
logoutUser();
}
void verifyInstructorsCannotAccess(String... params) {
loginAsInstructor("instructor-googleId");
verifyCannotAccess(params);
logoutUser();
}
void verifyAnyInstructorCanAccess(Course currentCourse, String... params) {
Instructor testInstructor = getTypicalInstructor();
testInstructor.setCourse(currentCourse);
verifyInstructorsCanAccess(currentCourse, params);
verifyInstructorsOfTheSameCourseCanAccess(currentCourse, params);
verifyInstructorsOfOtherCoursesCanAccess(currentCourse, params);
verifyAccessibleForAdminsToMasqueradeAsInstructor(testInstructor, params);
}
void verifyAnyNonMasqueradingInstructorCanAccess(Course currentCourse, String... params) {
Instructor testInstructor = getTypicalInstructor();
testInstructor.setCourse(currentCourse);
verifyInstructorsCanAccess(currentCourse, params);
verifyInstructorsOfTheSameCourseCanAccess(currentCourse, params);
verifyInstructorsOfOtherCoursesCanAccess(currentCourse, params);
verifyInaccessibleForAdminsToMasqueradeAsInstructor(testInstructor, params);
}
void verifyInstructorsOfTheSameCourseCanAccess(Course currentCourse, String... params) {
loginAsInstructorOfTheSameCourse(currentCourse);
verifyCanAccess(params);
Instructor otherCourseInstructor = getTypicalInstructor();
Course otherCourse = new Course("other-course-id", "other-course-name", Const.DEFAULT_TIME_ZONE, "teammates");
otherCourseInstructor.setCourse(otherCourse);
Student sameCourseStudent = getTypicalStudent();
sameCourseStudent.setCourse(currentCourse);
verifyCannotMasquerade(otherCourseInstructor.getId().toString(), params);
verifyCannotMasquerade(sameCourseStudent.getId().toString(), params);
}
void verifyInstructorsOfOtherCoursesCanAccess(Course currentCourse, String... params) {
loginAsInstructorOfOtherCourse();
verifyCanAccess(params);
Instructor sameCourseInstructor = getTypicalInstructor();
sameCourseInstructor.setCourse(currentCourse);
Student sameCourseStudent = getTypicalStudent();
sameCourseStudent.setCourse(currentCourse);
verifyCannotMasquerade(sameCourseInstructor.getId().toString(), params);
verifyCannotMasquerade(sameCourseStudent.getId().toString(), params);
}
void verifyInstructorsOfOtherCoursesCannotAccess(String... params) {
loginAsInstructorOfOtherCourse();
verifyCannotAccess(params);
}
void verifyAccessibleWithModifySessionPrivilege(Course thisCourse, String... params) {
verifyAccessibleWithCorrectSameCoursePrivilege(
thisCourse, Const.InstructorPermissions.CAN_MODIFY_SESSION, params);
}
void verifyInaccessibleWithoutModifySessionPrivilege(Course thisCourse, String... params) {
verifyInaccessibleWithoutCorrectSameCoursePrivilege(
thisCourse, Const.InstructorPermissions.CAN_MODIFY_SESSION, params);
}
void verifyAccessibleWithSubmitSessionInSectionsPrivilege(Course thisCourse, String... params) {
verifyAccessibleWithCorrectSameCoursePrivilege(
thisCourse, Const.InstructorPermissions.CAN_SUBMIT_SESSION_IN_SECTIONS, params);
}
void verifyInaccessibleWithoutSubmitSessionInSectionsPrivilege(Course thisCourse, String... params) {
verifyInaccessibleWithoutCorrectSameCoursePrivilege(
thisCourse, Const.InstructorPermissions.CAN_SUBMIT_SESSION_IN_SECTIONS, params);
}
void verifyAccessibleWithCorrectSameCoursePrivilege(
Course thisCourse, String privilege, String... params) {
verifySameCourseAccessibility(thisCourse, privilege, true, params);
verifyDifferentCourseAccessibility(thisCourse, privilege, false, params);
}
void verifyAccessibleWithCorrectSameCoursePrivilege(
Course thisCourse, InstructorPrivileges privilege, String... params) {
verifySameCourseAccessibility(thisCourse, privilege, true, params);
verifyDifferentCourseAccessibility(thisCourse, privilege, false, params);
}
void verifyInaccessibleWithoutCorrectSameCoursePrivilege(
Course thisCourse, String privilege, String... params) {
verifySameCourseAccessibility(thisCourse, privilege, false, params);
verifyDifferentCourseAccessibility(thisCourse, privilege, false, params);
}
void verifyInaccessibleWithoutCorrectSameCoursePrivilege(
Course thisCourse, InstructorPrivileges privilege, String... params) {
verifySameCourseAccessibility(thisCourse, privilege, false, params);
verifyDifferentCourseAccessibility(thisCourse, privilege, false, params);
}
// Students
void verifyStudentsCanAccess(String... params) {
loginAsStudent("student-googleId");
verifyCanAccess(params);
logoutUser();
}
void verifyStudentsCannotAccess(String... params) {
loginAsStudent("student-googleId");
verifyCannotAccess(params);
logoutUser();
}
void verifyStudentsOfTheSameCourseCanAccess(Course thisCourse, String... params) {
loginAsStudentOfTheSameCourse(thisCourse);
verifyCanAccess(params);
}
void verifyStudentsOfOtherCoursesCannotAccess(Course thisCourse, String... params) {
loginAsStudentOfOtherCourse();
verifyCannotAccess(params);
}
// Unregistered
void verifyUnregisteredCanAccess(String... params) {
loginAsUnregistered("unregistered-googleId");
verifyCanAccess(params);
logoutUser();
}
void verifyUnregisteredCannotAccess(String... params) {
loginAsUnregistered("unregistered-googleId");
verifyCannotAccess(params);
logoutUser();
}
// No login
void verifyWithoutLoginCanAccess(String... params) {
logoutUser();
verifyCanAccess(params);
}
void verifyWithoutLoginCannotAccess(String... params) {
logoutUser();
verifyCannotAccess(params);
}
/*
* Mid-level access control tests: Test an action's access control for a single
* user type.
*
* - Use a high-level test whenever possible instead of a mid-level test.
* - Use when a focused check is needed or to reduce redundancy.
* - Follow the same user type order as high-level tests when adding new access
* control tests.
*/
주석의 내용을 살펴보면, 특정 사용자 유형 하나를 딱 집어서 접근 가능한지 불가능한지를 확인하는 메서드다. 먼저 위의 High-level 메서드로 모든 역할에 대한 접근 권한 흐름을 검증하고, 추가로 특정 케이스를 한층 더 깊게 살펴보거나 중복을 줄이고 싶을 때 이 Mid-level 메서드들을 사용하면 된다는 얘기인 것 같다. 만약 새로운 테스트를 추가할 때는 High-level에서 제시된 접근 권한 순서를 준수하며 작성해야 하는 것 같다.
테스트 코드 작성에 관한 지침도 상세하게 작성되어 있었다.

간단하게 요약하자면,
3주차 과제에서 시도해봤던 테스트 주도 개발 방법론을 적용하여 테스트 코드를 작성해야 한다. 근데 이건 새로운 기능을 추가할 경우에 적용하라는 얘기인 것 같다. 지금은 마이그레이션된 테스트 코드들을 리팩토링 하고, 다른 예외적인 상황이 있는지 생각해보는 것으로도 충분하다고 생각했다.
각 테스트는 서로 영향 주지 않게 독립적인 테스트가 되어야 한다.
하나의 거대한 테스트 코드를 작성하는 것이 아니라 작은 단위 테스트 여러 개를 작성해야 한다. 우테코에서도 강조되었던 부분이고, 실제 피드백까지 받았던 내용이다.
이미 검증한 것을 또 검증하지 말아야 한다.
커버리지 100%를 지향해야 하고, 여기서 만족하지 말아야 한다.
각각의 실행 단위에 맞는 레벨로 테스트를 해야 하는데, 내가 해야 할 작업에 대입해보면, 각 접근 주체에 대해 High-level 메서드를 우선적으로 고려하고, 개별 테스트가 추가로 필요할 때 Mid-level 메서드를 도입해야 한다는 의미인 것 같다.
private 메서드는 직접 접근하지 말고, 그 클래스를 원래 사용하던 방식으로 간접적으로 실행하면 충분하다는 말인 것 같다.
테스트 커버리지는 소프트웨어 테스트가 전체 시스템의 코드와 기능을 어느 정도까지 검증했는지를 측정하는 지표다. 이를 통해 테스트가 누락된 영역을 식별하고, 소프트웨어의 품질과 안정성을 보장할 수 있다. 테스트 커버리지의 주요 목적을 나열하면 아래와 같다.
테스트 커버리지는 ISO/IEC 29119 국제 표준에서 권장하는 소프트웨어 테스트 절차 중 테스트 분석 및 평가 단계에 해당하며, SDLC(Software Development Life Cycle)의 테스트 및 검증 단계에서 핵심적으로 사용된다.
테스트 커버리지의 주요 유형을 말해보자면,
라인 커버리지(Line Coverage): 소스 코드의 각 라인이 최소 한 번 이상 실행되었는지 확인브랜치 커버리지(Branch Coverage): 조건문과 분기문이 모든 가능한 경로로 실행되었는지 확인함수 커버리지(Function Coverage): 모든 함수와 메서드가 호출되었는지 확인조건 커버리지(Condition Coverage): 조건문의 각 요소가 참과 거짓을 모두 포함했는지 확인주어진 지침에 따르면, 이러한 부분들을 고려해서 테스트를 작성해서 커버리지 100%를 지향하라는 의미인 것 같다. TEAMMATES는 JaCoCo(Java Code Coverage)라는 도구를 사용해서 테스트 코드 실행 시 실행된 코드 라인, 분기, 메서드 등을 측정하고, 리포트를 생성해주는 것 같다. 테스트 코드를 모두 작성하고 나서 한 번 확인해보자.
본격적으로 테스트 코드를 작성하기 전, 기존에 수많은 단위 테스트들 중 몇 가지를 리팩토링한 다른 기여자의 Merge된 PR이 있길래 어떤 식으로 작성했는지 내부를 확인해봤다.
PR 링크 -> [#13304] Refactoring for Unit Test Access Controls by arnav-goel05
이 기여자는 8개의 단위 테스트를 리팩토링 했다. 몇몇 파일의 변경 사항을 확인했을 때, 확실히 여러 단위 테스트를 공용 테스트 메서드로 크게 간소화 한 것을 볼 수 있었다.

각 사용자 유형에 대해 일일이 로그인하고, 검증하던 코드를 BaseActionTest의 High-level 테스트 메서드 한 번으로 대체한 것을 볼 수 있다.

이제 내가 작업해야 할 GetStudentActionTest.java와 GetStudentsActionTest.java 파일 안에 있는 여러 개의 단위 테스트들을 testAccessControl()와 같이 하나의 테스트 메서드로 치환할 수 있으면 하고, 그렇지 않은 경우에는 각 기능에 대해 엄격한 단위 테스트를 진행하도록 하고, 내부에서 High-level 메서드를 우선적으로 도입해서 리팩토링 하도록 하자.