유닛테스트 베스트 프랙티스 / 하는 법 / 방법 / 좋은 유닛테스트 / unittest / testcase best practice /
Unit test best practices in C#
ref.1 의 내용을 간략히 정리했다.
좋은 unit test 의 특징
- 빠르다.
- 독립적이다. 다른 것에 의존성이 없다.
- 반복적으로 수행할 수 있어야 한다.
- pass , fail 을 자동으로 감지할 수 있어야 한다.
- 테스트코드 작성 시간이 길지 않아야 한다. : 테스트하려는 code 를 작성하는 것보다 테스트 작성에 더 많은 시간이 걸리는 것은 새로운 테스트 디자인을 고민해 봐야 한다.
Code coverage
’코드 커버리지’가 높은것이 꼭 좋은 코드를 이야기하지 않는다. 예를 들어 edge cases 등을 잘 다루지 않았다면, coverage 가 높아도 의미가 없다. 무조건 높은 coverage 를 추구하는 것은 역효과이다.
code coverage는 커버된 line이나 branch들이 올바르게 테스트되었음을 보장하는 것이 아니라 테스트에서 실행되었음을 보장할 뿐이다.
code coverage 100% 에 집착할때의 역효과:
- 보안에 대한 잘못된 인식
- 유지관리해야 할 가치가 낮은 테스트로 인한 문제
- machine cycle들을 낭비한다.
- 기술부채를 생성한다.
테스트 누락으로 인해 나쁜코드가 production 에 push 되는 경우:
- code coverage analysis 로 쉽게 식별할 수 있는 코드의 특정 경로를 테스트에서 다루지 않았거나
- code coverage가 있는 영역의 특정 edge case 를 테스트에서 다루지 않았기 때문이다. 이 edge case 는 code coverage analysis으로 파악하기 어렵거나 불가능하다.
테스트가 cover하는 line 이 적절하게 테스트를 실행하고 있는지, 적절하게 실패를 assert 하는지를 평가하는 더 나은 방법은 mutation testing 이다.
코드 커버리지가 낮으면, 많은 영역이 테스트되지 않고 배포됐다는 이야기인데, 이는 나쁜코드가 production 환경으로 들어갈 위험을 증가시킨다.
Mock 과 Stub 의 차이
Mock 은 그 자체를 테스트 하게 되는 fake object 이다. 예를 들면 아래 코드에서 처럼 mock 의 값이 변경되었는지를 assert 를 이용해서 확인한다.
var mock
var mockOrder = new FakeOrder();
var purchase = new Purchase(mockOrder);
purchase.ValidateOrders();
Assert.True(mockOrder.Validated);
반면에 Stub 은 그저 test 를 도울뿐 그자체가 test 되지 않는다.
var stubOrder = new FakeOrder();
var purchase = new Purchase(stubOrder);
purchase.ValidateOrders();
Assert.True(purchase.CanBeShipped);
Best practices
- test 이름을 잘 짓자
- 테스트를 잘 배열하자.
- 최소한의 통과하는 테스트를 작성하자.
- magic 문자열을 피하자.
- 테스트내에 logic 을 피하자.
- helper method 들을 사용하자.
- 여러개의 assert 를 사용하는 것을 피하자.
- private method 의 검증은 public method 들을 unit test 하는 것으로 검증하자.
- static references 들을 Stub 으로 사용하자.
1. test 이름을 잘 짓자
이름이 테스트의 의도를 명확하게 표현하기 때문이다.
이름은 다음 3가지를 포함하자
- 테스트되는 method 이름
- 테스트되는 시나리오
- 시나리오가 살행될 때 기대되는 행동
// - Add : method name
// - SingleNumger: 테스트 시나리오
// - ReturnsSameNumber : expected behavior
public void Add_SingleNumber_ReturnsSameNumber(){
...
}
2. 테스트를 잘 배열하자.
일반적으로 unit test 가 Arrange, Act, Assert 로 진행된다. 이것을 명확히 읽을 수 있게 해주자. 테스트를 가능한 잘 읽을 수 있도록 해주자. Arrange, Assert 로 부터 무엇을 테스트하려는 것인지가 명확히 확인할 수 있도록 해줘야 한다.
3. 최소한의 통과하는 테스트를 작성하자.
테스트 코드작성시에는 동작(behavior)에 초점을 두자.
테스트하려는 동작을 증명하기 위해 최대한 간소하게 작성하자. model 의 properties 중 “테스트와 관련없는 property를 set 하는 것”이나 필요치 않은 “non-zero 값을 사용하는 것”들은 테스트하려는 것들에 대한 집중을 흐트러뜨릴 뿐이다.
non-zero 값을 사용하는 것 이것은 일반적으로 어떤 값을 넣어도 되는 테스트일때 굳이 이상한 값을 넣어서 왜 이런 값을 넣었는지 궁금하게 만들필요가 없다는 뜻 인듯 하다.
4. 특정의미가 있는 문자열을(magic string)을 쓰지말자.
unit test 에서 변수명을 짓는 것도 중요하다.
이 테스트로 무엇을 달성하려고 하는지 보다 ’무엇을 증명하려는지’를 명확하게 보여줘야 한다. 되도록 test code 를 읽는 사람이 이 값이 무슨 뜻인지 알기 위해 원본 source code 를 보지않아도 이해할 수 있도록 해줘야 한다.
만약 특정 문자열을 사용해야 하는 경우라면, 아래처럼 변수명을 정해주고 사용하자.
void Add_MaximumSumResult_ThrowsOverflowException()
{
var stringCalculator = new StringCalculator();
const string MAXIMUM_RESULT = "1001";
Action actual = () => stringCalculator.Add(MAXIMUM_RESULT);
Assert.Throws<OverflowException>(actual);
}
5. 테스트 안에서 logic 을 피하자.
수동으로 string 을 묶거나, 논리적인 조건들(if, while, for, switch) 을 피하자.
이것으로 테스트 코드에서 버그의 발생을 줄일 수 있다. 그것의 구현의 세부사항보다는 결과에 집중하자.
테스트가 실패했다는 것이 실제로 code 가 뭔가 이상하다는 것을 알려줘야 한다.(me: 테스트가 코드의 logic 을 잘 테스트해야 한다.는 뜻인듯.)
만약에 input 에 대한 결과값들을 여러번 테스트하고 싶다면, 이것을 for 문으로 만들어서 테스트하지 말고, 여러개의 테스트로 분리해서 테스트하자.
6. setup, teardown 보다는 helper method 를 선호하자.
- helper method 가 가독성을 높여준다. setup/teardown 은 test code 내에 보이지 않지만 helper method 는 보인다.
- 너무 많은 것은 setup 하거니, 너무 적게 setup 하지 않게 된다.
- test 사이에 state 를 공유할 확률이 적다. state 를 공유하는 것은 그 test 사이에 원치않는 dependencies 를 만들게 된다.
7. 여러개의 assert 는 피하자.
1개의 assert 가 실패하면 그 뒤의 assert 도 같이 실패해 버린다.(me: 이것은 자동으로 계속 돌아야 하는 경우 안좋다. 전부테스트하고 틀린것들을 찾아내야 하는데, 하나가 실패해서 다른 것들을 전혀 테스트 안하게 된다.)
8. private method 들은 public method 들을 테스트하는 것으로 검증하자.
- private method 는 public method 의 자세한 구현이기 때문에 public method 를 test 하면 private 도 같이 테스트된다.
- me : private method 는 refacotring 등을 하면서 구조적으로 자주 변경될 수 있다.
9. static 참고들을 Stub 하자.
production code 가 static references 들(에를 들면 DateTime.Now 같은 함수들)을 이용하면, 테스트하기 힘들다. 이럴 때는 이 부분을 외부에서 parameter 로 넘겨서 받도로 한다. 그래서 stub 을 만들어서 넘기면 된다.
public int GetDiscountedPrice(int price)
{
if (DateTime.Now.DayOfWeek == DayOfWeek.Tuesday)
{
return price / 2;
}
else
{
return price;
}
}
public interface IDateTimeProvider
{
DayOfWeek DayOfWeek();
}
public int GetDiscountedPrice(int price, IDateTimeProvider dateTimeProvider)
{
if (dateTimeProvider.DayOfWeek() == DayOfWeek.Tuesday)
{
return price / 2;
}
else
{
return price;
}
}
댓글 없음:
댓글 쓰기