Apex 유닛 테스트 시작하기
학습 목표
이 유닛을 완료하면 다음을 수행할 수 있습니다.
- Apex 유닛 테스트의 주요 이점을 설명할 수 있습니다.
- 테스트 메서드로 클래스를 정의할 수 있습니다.
- 클래스의 모든 테스트 메서드를 실행하고 실패를 검사할 수 있습니다.
- 테스트 클래스 모음을 만들고 실행할 수 있습니다.
Apex 유닛 테스트
Apex 테스트 프레임워크를 사용하면 Lightning Platform에서 Apex 클래스 및 트리거에 대한 테스트를 작성하고 실행할 수 있습니다. Apex 유닛 테스트는 Apex 코드의 고품질을 보장하고 Apex 배포 요구 사항을 충족할 수 있도록 합니다.
테스트는 성공적인 장기 개발의 주요한 요소이며 개발 프로세스의 중요한 구성 요소입니다. Apex 테스트 프레임워크를 사용하면 Apex 코드를 쉽게 테스트할 수 있습니다. Apex 코드는 생산이 아닌 sandbox 환경 또는 개발자 조직에서만 작성할 수 있습니다. Apex 코드는 sandbox에서 프로덕션 조직에 배포할 수 있습니다. 또한 앱 개발자는 패키지를 Lightning Platform AppExchange에 업로드하여 개발자 조직에서 고객에게 Apex 코드를 배포할 수 있습니다. 품질 보증에 중요할 뿐만 아니라 Apex 유닛 테스트는 Apex 배포 및 배포를 위한 요구 사항이기도 합니다.
다음은 Apex 유닛 테스트의 이점입니다.
- Apex 클래스 및 트리거가 예상대로 작동하는지 확인 가능
- 클래스 및 트리거가 업데이트될 때마다 다시 실행할 수 있는 회귀 테스트 도구 모음을 보유하여 앱에 대한 향후 업데이트로 인해 기존 기능이 중단되지 않도록 할 수 있음
- Apex를 프로덕션에 배포하거나 패키지를 통해 고객에게 Apex를 배포하기 위한 코드 적용 범위 요구 사항을 충족할 수 있음
- 프로덕션 사용자의 생산성을 높이는 프로덕션 조직에 고품질 앱을 제공할 수 있음
- 패키지 가입자에게 제공되는 고품질 앱으로 고객의 신뢰를 높일 수 있음
배포를 위한 코드 적용 요구 사항
Lightning Platform AppExchange용으로 코드를 배포하거나 패키지하려면 먼저 Apex 코드의 75% 이상이 테스트에 포함되어야 하며 이러한 모든 테스트를 통과해야 합니다. 또한 각 트리거에는 어느 정도 적용 범위가 있어야 합니다. 코드 적용은 배포의 요구 사항이지만 이 요구 사항을 충족하기 위해서만 테스트를 작성하지 마세요. 긍정 및 부정 테스트 사례, 대량 및 단일 레코드 처리를 포함하여 앱의 일반적인 사용 사례를 테스트해야 합니다.
테스트 메서드 구문
테스트 메서드는 @isTest
주석을 사용하여 정의되고 다음 구문을 사용합니다.
@isTest static void testName() { // code_block }
@isTest
주석은 괄호 안에 여러 수정자를 포함하고 공백으로 구분합니다. 이후에 해당 매개변수를 다루겠습니다.
테스트 메서드의 가시성은 중요하지 않으므로 테스트 프레임워크가 항상 테스트 메서드에 액세스할 수 있으므로 테스트 메서드를 공개 또는 비공개로 선언해도 차이가 없습니다. 이러한 이유로 액세스 한정자는 구문에서 생략됩니다.
테스트 메서드는 @isTest
라는 주석이 달린 클래스인 테스트 클래스에 정의되어야 합니다. 이 샘플 클래스는 하나의 테스트 메서드가 있는 테스트 클래스의 정의를 보여줍니다.
@isTest private class MyTestClass { @isTest static void myTest() { // code_block } }
테스트 클래스는 비공개 또는 공개가 될 수 있습니다. 유닛 테스트용으로만 테스트 클래스를 사용하는 경우 비공개로 선언합니다. 공개 테스트 클래스는 일반적으로 테스트 데이터 팩토리 클래스에 사용되며 이후에 다뤄보겠습니다.
유닛 테스트 예제: TemperatureConverter 클래스 테스트
다음의 간단한 예제는 세 가지 테스트 메서드가 있는 테스트 클래스입니다. 테스트 중인 클래스 메서드는 화씨 온도를 입력으로 사용합니다. 이 온도를 섭씨로 변환하고 변환된 결과를 반환합니다. 사용자 정의 클래스와 테스트 클래스를 추가해 보겠습니다.
- Developer Console에서 File(파일) | New(새로 만들기) | Apex Class(Apex 클래스)를 클릭하고 클래스 이름에 대해
TemperatureConverter
를 입력한 다음 OK(확인)를 클릭합니다.
- 기본 클래스 본문을 다음으로 바꿉니다.
public class TemperatureConverter { // Takes a Fahrenheit temperature and returns the Celsius equivalent. public static Decimal FahrenheitToCelsius(Decimal fh) { Decimal cs = (fh - 32) * 5/9; return cs.setScale(2); } }
-
Ctrl+S를 눌러 클래스를 저장합니다.
- 이전 단계를 반복하여
TemperatureConverterTest
클래스를 만듭니다. 이 클래스에 대해 다음을 추가합니다.@isTest private class TemperatureConverterTest { @isTest static void testWarmTemp() { Decimal celsius = TemperatureConverter.FahrenheitToCelsius(70); System.assertEquals(21.11,celsius); } @isTest static void testFreezingPoint() { Decimal celsius = TemperatureConverter.FahrenheitToCelsius(32); System.assertEquals(0,celsius); } @isTest static void testBoilingPoint() { Decimal celsius = TemperatureConverter.FahrenheitToCelsius(212); System.assertEquals(100,celsius,'Boiling point temperature is not expected.'); } @isTest static void testNegativeTemp() { Decimal celsius = TemperatureConverter.FahrenheitToCelsius(-10); System.assertEquals(-23.33,celsius); } }
TemperatureConverterTest
테스트 클래스는 화씨 온도에 대해 다른 입력으로 메서드를 호출하여 메서드가 예상대로 작동하는지 확인합니다. 각 테스트 메서드는 따뜻한 온도, 어는점 온도, 끓는점 온도 및 음의 온도와 같은 입력 유형을 확인합니다. System.assertEquals()
메서드를 호출하여 유효성 검사를 수행하며, 여기에는 두 개의 매개변수를 사용하는데 첫 번째는 예상 값이고 두 번째는 실제 값입니다. 세 번째 매개변수를 사용하는 이 메서드의 다른 버전이 있으며, testBoilingPoint()
에서 사용되고 수행 중인 비교를 설명하는 문자열입니다. 어설션이 실패하면 이 선택적 문자열이 기록됩니다.
이 클래스의 메서드를 실행해 보겠습니다.
- Developer Console에서 Test(테스트) | New Run(새로운 실행)을 클릭합니다.
-
Test Classes(테스트 클래스)에서 TemperatureConverterTest를 클릭합니다.
-
TemperatureConverterTest
클래스의 모든 테스트 메서드를 테스트 실행에 추가하려면 Add Selected(선택 항목 추가)를 클릭합니다.
-
Run(실행)을 클릭합니다.
- Tests(테스트) 탭에서 실행 중인 테스트의 상태를 볼 수 있습니다. 테스트 실행을 확장하고 실행된 개별 테스트 목록이 표시될 때까지 다시 확장합니다. 모두 녹색 체크 표시가 있습니다.
테스트를 실행하면 조직의 Apex 클래스 및 트리거에 대한 코드 검사가 자동으로 생성됩니다. Developer Console의 Tests(테스트) 탭에서 코드 적용 범위 백분율을 확인할 수 있습니다. 이 예에서 테스트한 클래스인 TemperatureConverter
클래스는 이 이미지와 같이 100% 적용 범위를 갖습니다.
하나의 테스트 메서드가 TemperatureConverter
클래스의 전체 적용 범위로 이어지므로 코드의 품질을 보장하기 위해 다양한 입력을 테스트하는 것이 여전히 중요합니다. 즉, 모든 데이터 포인트를 확인할 수 없지만 공통 데이터 포인트와 다양한 입력 범위를 테스트할 수 있습니다. 예를 들어 양수 및 음수, 경계값, 잘못된 매개변수 값 전달을 확인하여 음의 동작을 확인할 수 있습니다. TemperatureConverter
클래스에 대한 테스트는 끓는 온도 및 음의 온도와 같은 공통 데이터 포인트를 확인합니다.
TemperatureConverterTest
테스트 클래스는 유효하지 않은 입력이나 경계 조건을 다루지 않습니다. 경계 조건은 최소값과 최대값에 관한 것입니다. 이 경우 온도 변환 방법은 Double
값보다 큰 대수를 허용할 수 있는 Decimal
을 허용합니다. 잘못된 입력의 경우 잘못된 온도는 없지만 유일한 잘못된 입력은 null입니다. 변환 방법은 이 값을 어떻게 처리하나요? 이 경우 Apex 런타임이 매개변수 변수를 역참조하여 수식을 평가할 때 System.NullPointerException
을 출력합니다. FahrenheitToCelsius()
메서드를 수정하여 잘못된 입력 및 null을 반환한 다음 테스트를 추가하여 잘못된 입력 동작을 확인할 수 있습니다.
여기까지 클래스 메서드에 사용된 변환 공식이 정확하므로 모든 테스트를 통과합니다. 하지만 그건 지루합니다. 어설션 실패 시 어떤 일이 발생하는지 보기 위해 실패를 시뮬레이션해 보겠습니다. 예를 들어 끓는점 온도 테스트를 수정하고 끓는점 섭씨 온도에 대한 잘못된 예상 값(100 대신 0)을 전달해 보겠습니다. 이로 인해 해당 테스트 메서드가 실패합니다.
-
testBoilingPoint()
테스트 메서드를 다음으로 변경합니다.@isTest static void testBoilingPoint() { Decimal celsius = TemperatureConverter.FahrenheitToCelsius(212); // Simulate failure System.assertEquals(0,celsius,'Boiling point temperature is not expected.'); }
- 동일한 테스트를 실행하려면 Tests(테스트) 탭에서 최신 실행을 클릭한 다음 Test(테스트) | Rerun(재실행)을 클릭합니다.
testBoilingPoint()
어설션에 실패하고 치명적인 오류(해결할 수 없는AssertException
)가 발생합니다.
- 최신 테스트 실행을 확장하여 Tests(테스트) 탭에서 결과를 확인합니다. 테스트 실행은 네 가지 테스트 중 하나가 실패했다고 보고합니다. 실패에 대한 자세한 내용을 보려면 테스트 실행을 두 번 클릭합니다. 자세한 결과는 이 이미지와 같이 별도의 탭에 표시됩니다.
[대체 텍스트: Developer Console에서 실패한 테스트 결과 검사]
- 테스트 실패에 대한 오류 메시지를 보려면 실패한 테스트에 대한 Errors(오류) 열 내부를 두 번 클릭합니다.
Assertion Failed:
(어설션 실패:) 옆에 있는 설명 텍스트는System.assertEquals()
문에서 제공한 텍스트입니다.System.AssertException:Assertion Failed:Boiling point temperature is not expected.:Expected:0, Actual:100.00
이러한 테스트 메서드의 테스트 데이터는 Salesforce 레코드가 아닌 숫자입니다. Salesforce 레코드를 테스트하는 방법과 다음 유닛에서 데이터를 설정하는 방법에 대해 자세히 알아보겠습니다.
코드 적용 범위 확장
테스트를 작성할 때 가능한 한 가장 높은 코드 범위로 확장해 보세요. Lightning Platform이 배포 및 패키지에 요구하는 가장 낮은 적용 범위인 75% 적용 범위를 목표로 하지 마세요. 테스트에서 다루는 테스트 사례가 많을수록 코드가 더 강력해질 가능성이 커집니다. 모든 클래스 메서드에 대한 테스트 메서드를 작성한 후에도 코드 적용 범위가 100%가 아닐 수도 있습니다. 한 가지 일반적인 원인은 조건부 코드 실행에 대한 모든 데이터 값을 포함하지 않는 것입니다. 예를 들어 일부 데이터 값은 클래스 메서드에 평가된 조건이 충족되는지 여부에 따라 다른 분기가 실행되도록 하는 if 문이 있는 경우 무시되는 경향이 있습니다. 테스트 메서드가 이러한 다른 값을 설명하는지 확인합니다.
이 예제에는 클래스 메서드 getTaskPriority()
가 포함되어 있으며, 여기에는 두 개의 if
문이 포함되어 있습니다. 이 메서드의 주요 작업은 제공된 리드 상태를 기반으로 우선 순위 문자열 값을 반환하는 것입니다. 이 메서드는 먼저 상태를 확인하고 상태가 잘못된 경우 null을 반환합니다. 상태가 CA인 경우 메서드는 'High'를 반환하며, 그렇지 않은 경우 다른 상태 값에 대해 'Normal'을 반환합니다.
public class TaskUtil { public static String getTaskPriority(String leadState) { // Validate input if(String.isBlank(leadState) || leadState.length() > 2) { return null; } String taskPriority; if(leadState == 'CA') { taskPriority = 'High'; } else { taskPriority = 'Normal'; } return taskPriority; } }
이는 getTaskPriority()
메서드에 대한 테스트 클래스입니다. 테스트 메서드는 단순히 하나의 상태('NY')를 가진 getTaskPriority()
를 호출합니다.
@isTest private class TaskUtilTest { @isTest static void testTaskPriority() { String pri = TaskUtil.getTaskPriority('NY'); System.assertEquals('Normal', pri); } }
Developer Console에서 이 테스트 클래스(TaskUtilTest
)를 실행하고 이 테스트가 다루는 해당 TaskUtil
클래스에 대한 코드 적용 범위를 확인합니다. 테스트 실행이 완료된 후 TaskUtil
에 대한 코드 적용 범위를 75%로 표시됩니다. Developer Console에서 이 클래스를 열 경우 이 이미지와 같이 6개의 파란색(적용됨) 라인과 2개의 빨간색(적용되지 않음) 라인이 표시됩니다.
라인 5가 다루어지지 않은 이유는 테스트 클래스에 잘못된 상태 매개변수를 전달하는 테스트가 포함되어 있지 않았기 때문입니다. 마찬가지로 테스트 메서드가 'CA'를 상태로 통과하지 못했으므로 라인 11은 다루지 않았습니다. 이러한 시나리오를 다루기 위해 두 가지 테스트 메서드를 더 추가해 보겠습니다. 다음은 testTaskHighPriority()
및 testTaskPriorityInvalid()
테스트 메서드를 추가한 후에 전체 테스트 클래스를 보여줍니다. Run All(모두 실행) 또는 New Run(새로운 실행)을 사용하여 이 테스트 클래스를 다시 실행할 경우 TaskUtil
에 대한 코드 적용 범위가 이제 100%가 됩니다.
@isTest private class TaskUtilTest { @isTest static void testTaskPriority() { String pri = TaskUtil.getTaskPriority('NY'); System.assertEquals('Normal', pri); } @isTest static void testTaskHighPriority() { String pri = TaskUtil.getTaskPriority('CA'); System.assertEquals('High', pri); } @isTest static void testTaskPriorityInvalid() { String pri = TaskUtil.getTaskPriority('Montana'); System.assertEquals(null, pri); } }
테스트 도구 모음 생성 및 실행
테스트 도구 모음은 함께 실행하는 Apex 테스트 클래스 모음입니다. 예를 들어 배포를 준비하거나 Salesforce에서 새 버전을 릴리스할 때마다 실행하는 테스트 도구 모음을 만듭니다. 정기적으로 함께 실행하는 테스트 클래스 집합을 정의하려면 Developer Console에서 테스트 도구 모음을 설정합니다.
이제 조직에 두 개의 테스트 클래스가 있습니다. 이 두 클래스는 관련이 없지만, 잠시 서로 관계가 있다고 가정해 보겠습니다. 이 두 테스트 클래스를 실행하고 싶지만 조직의 모든 테스트를 실행하고 싶지 않은 상황이 있다고 가정해 봅시다. 두 클래스를 모두 포함하는 테스트 제품군을 만든 다음 도구 모음에서 테스트를 실행합니다.
- Developer Console에서 Test(테스트) | New Suite(새로운 제품군)를 선택합니다.
- 제품군 이름으로
TempConverterTaskUtilSuite
를 입력한 다음 OK(확인)를 클릭합니다.
- Ctrl 키를 누른 상태에서 TaskUtilTest를 선택한 다음 TemperatureConverterTest를 선택합니다.
- 선택한 테스트 클래스를 제품군에 추가하려면 >를 클릭합니다.
-
Save(저장)를 클릭합니다.
-
Test(테스트) | New Suite Run(새로운 제품군 실행)을 선택합니다.
-
TempConverterTaskUtilSuite를 선택한 다음 >를 클릭하여
TempConverterTaskUtilSuite
를 Selected Test Suites(선택된 테스트 제품군) 열로 옮깁니다.
-
Run Suites(제품군 실행)를 클릭합니다.
- Tests(테스트) 탭에서 실행 중인 테스트의 상태를 모니터링합니다. 테스트 실행을 확장하고 실행된 개별 테스트 목록이 표시될 때까지 다시 확장합니다. 개별 테스트 메서드 실행과 마찬가지로 메서드 이름을 두 번 클릭하여 자세한 테스트 결과를 볼 수 있습니다.
테스트 데이터 생성
테스트 메서드에서 생성된 Salesforce 레코드는 데이터베이스에 실행되지 않습니다. 테스트 실행이 완료되면 롤백됩니다. 이 롤백 동작은 테스트가 실행된 후 테스트 데이터를 정리할 필요가 없으므로 테스트에 편리합니다.
기본적으로 Apex 테스트는 사용자 또는 프로필 개체와 같은 설정 및 메타데이터 개체에 대한 액세스를 제외하고 조직의 기존 데이터에 액세스할 수 없습니다. 테스트를 위한 테스트 데이터를 설정합니다. 테스트 데이터를 생성하면 테스트가 더욱 강력해지고 조직에서 데이터가 누락되거나 변경되어 발생하는 실패를 방지할 수 있습니다. 테스트 메서드에서 직접 테스트 데이터를 생성하거나 나중에 알게 될 유틸리티 테스트 클래스를 사용하여 테스트 데이터를 생성할 수 있습니다.
추가 정보
- Visual Studio Code용 Salesforce Apex 확장 프로그램을 사용하여 Apex 테스트를 실행하고 코드 기능을 확인할 수 있습니다.
- 각 조직에 최대 6MB의 Apex 코드를 저장할 수 있습니다.
@isTest
라는 주석이 달린 테스트 클래스는 이 제한에 포함되지 않습니다.
- 테스트 데이터가 롤백되더라도 테스트를 위해 별도의 데이터베이스를 사용하지 않습니다. 결과적으로 고유한 제약 조건이 있는 필드가 있는 일부 sObject의 경우 중복 sObject 레코드를 삽입하면 오류가 발생합니다.
- 테스트 메서드에서는 이메일을 보내지 않습니다.
- 테스트 메서드는 외부 서비스에 대한 콜아웃을 만들 수 없습니다. 테스트에서 모의 콜아웃을 사용할 수 있습니다.
- 테스트에서 수행된 SOSL 검색은 빈 결과를 반환합니다. 예측 가능한 결과를 얻으려면
Test.setFixedSearchResults()
를 사용하여 검색에 의해 반환될 레코드를 정의합니다.
리소스
-
Apex 개발자 가이드: 모범 사례 테스트
-
Apex 개발자 가이드: Apex 유닛 테스트란 무엇인가요?
-
Apex 개발자 가이드: 유닛 테스트의 조직 데이터에서 테스트 데이터 분리
-
Salesforce 도움말: 코드 적용 범위 확인
실습 과제 준비하기
이 유닛에 대한 실습 과제를 완료하려면 아래에서 복사한 코드가 포함된 VerifyDate
라는 새 Apex 클래스를 생성해야 합니다.
public class VerifyDate { //method to handle potential checks against two dates public static Date CheckDates(Date date1, Date date2) { //if date2 is within the next 30 days of date1, use date2. Otherwise use the end of the month if(DateWithin30Days(date1,date2)) { return date2; } else { return SetEndOfMonthDate(date1); } } //method to check if date2 is within the next 30 days of date1 private static Boolean DateWithin30Days(Date date1, Date date2) { //check for date2 being in the past if( date2 < date1) { return false; } //check that date2 is within (>=) 30 days of date1 Date date30Days = date1.addDays(30); //create a date 30 days away from date1 if( date2 >= date30Days ) { return false; } else { return true; } } //method to return the end of the month of a given date private static Date SetEndOfMonthDate(Date date1) { Integer totalDays = Date.daysInMonth(date1.year(), date1.month()); Date lastDay = Date.newInstance(date1.year(), date1.month(), totalDays); return lastDay; } }