아래 문서는 MS Doc를 참고하여 작성 하였습니다.
Best practices for writing unit tests - .NET
왜 unit test를 하는가?
- 테스트의 시간을 줄이기 위해서
- 기존 코드가 손상되는 위험으로부터 보호하기 위해서
- 실행가능한 문서로 정보 전달을 하기 위해서 :문서로써 함수의 정보를 주는 역할을 할 것이고 실제로 실행도 해 정확하게 알 수 있다.
- 항상 특정함수가 어떻게 동작하는지 어떤 결과를 만드는지 정확하게 알 수 없다. 이름이 잘 정의된 테스트의 Suite이 만들어 져 있다면
- 코드간 결합(커플링)을 방지하기 위해서:
- 단위테스트를 만들게 되면 자연스럽게 결합된 코드들을 나누어야 하는 과정이 생긴다.
Good unit test란?
- Fast : 어느정도 규모가 있는 프로젝트에서 수천개의 유닛테스트를 단시간내에(Miliseconds) 할 수 있어야 한다.
- Isolated : 테스트코드 자체만으로 동작해야 한다. 어떠한 외부요인(파일시스템, 데이터베이스등)으로 부터 독립적인 테스트가 가능해야 한다. → DB가 바뀌든 파일이 바뀌든 동일한테스트가 진행될 수 있어야 한다.
- Repeatable : 테스트시에 항상 같은 결과 값이 나와야 한다. → 테스트할 때마다 결과가 다르게 나와서는 안된다.
- Self-Checking : 테스트코드는 자동으로 pass or fail을 파악해야 한다.
- Timely : 테스트코드 작성하는 시간이 테스트 받는 코드 작성되는 시간보다 매우 오래 걸리면 안된다. → 오래 걸린다면 테스트 할 수 있는 더 작은 단위로 나눌 것을 고려하라 (이것은 빨리 코드를 짜야 한다는 말이 아니다. 단위를 쪼개라는 말이다.)
Code coverage
높은 코드 커버리지는 높은 퀄리티의 코드로 보통 여겨진다. 하지만 이 값은 퀄리티가 좋다고 단정지을 수 없다. 다만 높은 코드 커버리지는 많은 양의 테스트코드가 실행됐다는 것이다.
커버리지의 %에 집중하는 것은 비생산적일 수 있다.
(그렇다면 적정 커버리지 퍼센트는 어느정도일까?, 커버리지확률은 어떤식으로 계산이 되는 것일까?)
cf) code coverage : 전체 코드에서 테스트 코드로 인해 실행되어지는 코드의 양 / 전체 코드* 100
용어 정리
테스트 코드에서 주로 사용 하는 용어들이 있다. fake, mock, stub 등을 사용하는데 MS docs에 그 용어에 대해 정리를 해 놨다. (용어들이 구분되어 쓰일 수 있도록)
아래의 설명은 용어의 개념에 대해 설명하고 있다.
Important It's important to get this terminology correct. If you call your stubs "mocks", other developers are going to make false assumptions about your intent.
용어를 잘못 사용하는 것이 product 코드를 망치거나 하지는 않는다.
하지만 문서에 나온 것과 같이 알맞은 용어를 사용하지 않으면 다른 개발자들이 당신이 만든 테스트 코드의 의도를 잘못 해석할 것이다. 그러므로 중요하다
- fake : fake는 가장 큰 개념의 용어이다. fake는 사용 의도에 따라 stub이 되기도 하고 mock 이 되기도 한다.
- mock : 유닛테스트에서 pass or fail의 최종결과를 나타내기위한 용어로 사용된다. → assert에서 결과적으로 사용
- stub : 유닛테스트에서의 object들간의 의존성을 대체하기 위해 사용되어지는 용어이다. → 연결고리역할, 의존성 주입
ex) Stub 에 대하여
/* 의도 : FakeOrder 인스턴스를 Purchase인스턴스 생성을 하기 위해 쓸때 */
var **stubOrder** = new FakeOrder();
var purchase = new Purchase(**stubOrder**); // stub은 FakeOrder의 인스턴스가 Purchase 생성을 위해 주입되어지는 변수로 사용
purchase.ValidateOrders();
Assert.True(purchase.CanBeShipped);
ex) Mock 에 대하여
/* 의도 : FakeOrder 인스턴스를 검사 하기 위해 쓸때 */
var **mockOrder** = new FakeOrder();
var purchase = new Purchase(mockOrder);
purchase.ValidateOrders();
Assert.True(**mockOrder**.Validated); // 테스트의 결과를 만들기위해 쓰여짐
Best Practices (링크)
테스트명 짓는 법 (Naming your tests)
테스트명은 3가지 부분으로 구성돼져야 한다.
- 테스트가되는 메서드명
- 어떤 테스트를 하는지에 대한 시나리오 내용
- 테스트시 발현돼야하는 결과
(파이썬에서는 ‘test’키워드가 가장 앞에 있어야 함)
이 세가지가 필요한 이유는 테스트의 의도를 명시적으로 나타낼 수 있기 때문.
테스트코드는 문서로써의 역할도 한다. 직접 내부 코드를 보지 않고도 테스트코드만으로 역할을 알 수 있다.
만약 테스트 코드가 실패했을 때라면 어떤 시나리오에서 조건을 만족하지 않았는지도 예측할 수 있어서 버그Fix에도 도움을 준다.
BAD:
[Fact]
public void **Test_Single**()
{
var stringCalculator = new StringCalculator();
var actual = stringCalculator.Add("0");
Assert.Equal(0, actual);
}
Better:
[Fact]
public void **Add_SingleNumber_ReturnsSameNumber**() // <- Naming 룰에 맞춘 메서드명 사용
{
var stringCalculator = new StringCalculator();
var actual = stringCalculator.Add("0");
Assert.Equal(0, actual);
}
테스트 단계를 만들어라 (Arranging your tests)
Arrange, Act, Assert는 일반적인 유닛테스트 패턴이다. 이름에서 알 수 있듯이 각각은 역할이 있다.
- Arrange : 테스트를 위한 준비하기
- Act : 테스트하고자하는 메서드 실행하기
- Assert : 검사하기
단계를 만드는 이유는 테스트 되는 핵심코드와 부수적인 것들을 명확하게 구분하기 위함이다.
또한 가독성을 높이기 위함이다.
Bad:
[Fact]
public void Add_EmptyString_ReturnsZero()
{
// Arrange
var stringCalculator = new StringCalculator();
// Assert
Assert.Equal(0, stringCalculator.Add(""));
}
Better:
[Fact]
public void Add_EmptyString_ReturnsZero()
{
**// Arrange**
var stringCalculator = new StringCalculator();
**// Act** <- 실제 테스트가 돼야 하는 코드
var actual = stringCalculator.Add("");
**// Assert**
Assert.Equal(0, actual);
}
가장작은 입력값 사용하기(Write minimally passing tests)
테스트에 들어가는 코드는 최대한 간단해야 한다. 테스트 하려는 동작만 확인될 수 있게 해야 한다.
테스트가 통과하기 위한 코드에 더많은 정보가 포함된다면(없어도 되는 코드) 테스트 의도를 명확하게 전달하지 못할 뿐 아니라 테스트시 에러를 발생시킬 수 있다.
Bad:
[Fact]
public void Add_SingleNumber_ReturnsSameNumber()
{
var stringCalculator = new StringCalculator();
// Input 값으로 가장 작은 값이 아닌 일반적인 크기의 수가 입력됐다
var actual = stringCalculator.Add("42");
Assert.Equal(42, actual);
}
Better:
[Fact]
public void Add_SingleNumber_ReturnsSameNumber()
{
var stringCalculator = new StringCalculator();
// 테스트 코드의 의도를 훼손하지 않도록 입력할 수 있는 값중 가장 작은 값을 입력한다
var actual = stringCalculator.Add("0");
Assert.Equal(0, actual);
}
유닛테스트에 String을 쓰지 말것 (Avoid magic strings)
테스트 코드에 직접 String을 사용할 경우 가독성을 떨어뜨린다. → 변수로 만들어 사용하라
Bad:
[Fact]
public void Add_BigNumber_ThrowsException()
{
var stringCalculator = new StringCalculator();
Action actual = () => stringCalculator.Add(**"1001"**);
Assert.Throws<OverflowException>(actual);
}
Better:
[Fact]
void Add_MaximumSumResult_ThrowsOverflowException()
{
var stringCalculator = new StringCalculator();
**const string MAXIMUM_RESULT = "1001"; // String을 변수에 넣어 사용한다**
Action actual = () => stringCalculator.Add(**MAXIMUM_RESULT**);
Assert.Throws<OverflowException>(actual);
}
논리코드를 이용하지 말것(Avoid logic in tests)
if, while, for, switch 문을 사용하지말것.
논리코드를 사용하면 테스트코드 구현에 집중을 하게 될 수 있으므로 가독성을 해친다.
만약 논리코드를 사용하는게 불가피 하다면 테스트를 분리하여 작성을 하라.
Bad:
[Fact]
public void Add_MultipleNumbers_ReturnsCorrectResults()
{
var stringCalculator = new StringCalculator();
var expected = 0;
var **testCases** = new[]
{
"0,0,0",
"0,1,2",
"1,2,3"
};
**foreach** (var test in testCases)
{
Assert.Equal(expected, stringCalculator.Add(test));
expected += 3;
}
}
Better:
[Theory]
// 논리가 필요한 경우라면 각각 따로 작성할 것
**[InlineData("0,0,0", 0)]
[InlineData("0,1,2", 3)]
[InlineData("1,2,3", 6)]**
public void Add_MultipleNumbers_ReturnsSumOfNumbers(string input, int expected)
{
var stringCalculator = new StringCalculator();
var actual = stringCalculator.Add(input);
Assert.Equal(expected, actual);
}
SetUp, TearDown 메서드 보다는 Helper메서드를 활용할 것 (Prefer helper methods to setup and teardown)
테스트 코드에는 Setup 메서드와 Teardown 메서드가 보통 내장 돼 있을 텐데 이 동작을 테스트에서 사용하기 보다는 Helper 메서드를 따로 만들어서 사용하기를 권장.
cf) setup, teardown은 보통 각각의 테스트들이 시작하기 전 끝난 후 공통적,반복적으로 동작하는 코드이다.
helper를 쓰는게 나은 이유?
- 다른 테스트를 위한 코드가 혼합돼있지 않기 때문에 혼돈을 피할 수 있다. (반복적으로 모든 테스트에서 실행되므로 여러 조건들이 혼재있는 것을 피한다)
- 필요한 세팅만 할 수 있다.
- 원치않는 테스트 상태를 다른 테스트코드와 공유하지 않을 수 있다.
Bad:
private readonly StringCalculator stringCalculator;
public StringCalculatorTests()
{
// SetUp
**stringCalculator = new StringCalculator();**
}
// more tests...
[Fact]
public void Add_TwoNumbers_ReturnsSumOfNumbers()
{
// 테스트코드만으로 stringCalculator에 대한 정보를 알 수 없다.
// 그러므로 Setup 부분을 읽어야 확인이 가능하기 때문에 Helper 함수를 이용해야 한다.
**var result = stringCalculator.Add("0,1");**
Assert.Equal(1, result);
}
Better:
[Fact]
public void Add_TwoNumbers_ReturnsSumOfNumbers()
{
// Helper함수로 인스턴스 생성
**var stringCalculator = CreateDefaultStringCalculator();**
var actual = stringCalculator.Add("0,1");
Assert.Equal(1, actual);
}
// more tests...
**// Helper method : 테스트를 위한 새로운 인스턴스 생성
private StringCalculator CreateDefaultStringCalculator()
{
return new StringCalculator();
}**
테스트 1개당 1개의 테스트를 하라 (Avoid multiple acts)
테스트에 따라 분리된 act를 작성해야 한다.
다른 조건으로 같은 테스트를 진행하고자 한다면 테스트코드 메서드의 파라미터를 활용하라.
한 테스트 코드 내에서 다중 테스트를 진행하면 안되는 이유는 :
- 테스트가 실패했을 때 어떤 테스트가 실패했는지 알 수 없다
- 테스트는 단일 케이스에 대해서만 진행돼야 한다.
- 테스트가 실패할 경우 테스트에 포함된 전체코드(다른테스트포함)를 생각해야하므로 낭비이다.’
Bad:
[Fact]
public void Add_EmptyEntries_ShouldBeTreatedAsZero()
{
**// 하나의 테스트에 여러개의 Act와 Assert를 삽입하지 말것**
// Act
var actual1 = stringCalculator.Add("");
var actual2 = stringCalculator.Add(",");
// Assert
Assert.Equal(0, actual1);
Assert.Equal(0, actual2);
}
Better:
[Theory]
**[InlineData("", 0)]
[InlineData(",", 0)]**
public void **Add_EmptyEntries_ShouldBeTreatedAsZero(string input, int expected)**
{
// Arrange
var stringCalculator = new StringCalculator();
// Act
var actual = stringCalculator.Add(input);
// Assert
Assert.Equal(expected, actual);
}
private 메서드는 public 메서드로 사용될 때 검증하라 (Validate private methods by unit testing public methods)
private 메서드는 public 메서드로 사용되는 시점을 찾아 public 메서드를 테스트 해야 한다.
public string ParseLogLine(string input)
{
var sanitizedInput = **TrimInput**(input);
return sanitizedInput;
}
**private** string **TrimInput**(string input)
{
return input.Trim();
}
위와 같은 코드가 있을 때 TrimInput 메서드부터 테스트 코드를 작성하려 할 것이다.
하지만 이 메서드는 private 메서드로 테스트 할 수 없다.
그러므로 ParseLogLine을 구현하기 위해 TripmInput 이 이용되는 시점을 이용해야 한다.
TrimInput은 쓰여지는 메서드 내에서 sanitizedInput을 변경 시킨다. 이때 변경된 내용을 검증하면 된다. (아래코드와 같다)
public void ParseLogLine_StartsAndEndsWithSpace_ReturnsTrimmedResult()
{
var parser = new Parser();
var result = parser.ParseLogLine(" a ");
Assert.Equals("a", result);
}
통제가 안되는 정적 참조변수를 Stub으로 만들어 의존성 주입해 사용하라 (Stub static references)
단위테스트는 독립적으로 테스트 코드안에서 모든 통제권을 가져야 한다.
하지만 production 코드에서 static 참조로 DateTime.Now 같은 값을 쓴다면 문제가 일어날 수 있다. (통제가 안되기 때문에)
public int GetDiscountedPrice(int price)
{
if (DateTime.Now.DayOfWeek == DayOfWeek.Tuesday)
{
return price / 2;
}
else
{
return price;
}
}
위의 코드는 해당 요일에 따라 다른 price가 리턴된다. 이말인 즉 이 메서드로 테스트 코드를 만들면 테스트 하는 날마다 값이 달라진다 (Repeatable 위반)
DateTime 값을 인터페이스로 받아 올 수 있도록 하여, 이 인터페이스를 Stub으로 이용할 수 있도록 한다. (의존성 주입)
인터페이스로 DateTime 값을 만들기 때문에 값을 Fix하여 테스트 할 수 있게 된다.
댓글