단위 테스트를 더 쉽고 쉽게 유지 관리할 수 있는 코드를 설계하는 방법에 대한 실용적인 팁
27109 단어 cleancodecodedesingsoftwareunittests
1. 의존성 주입 사용
간단히 말해서 의존성 주입은 객체를 다른 객체로 전달하는 것입니다. 예를 들어:
class Receiver(IDependcyClass dependencyClass){
}
이 디자인 패턴을 사용하면 테스트 중인 클래스에 모의 동작을 주입할 수 있으므로 단위 테스트가 훨씬 쉬워집니다. 의존성 주입을 더 잘 이해하고 싶다면 James Shore의 게시물http://www.jamesshore.com/v2/blog/2006/dependency-injection-demystified을 읽는 것이 좋습니다.
2. 함수 홀더 클래스에서 함수 실행기 분리
함수를 실행하는 클래스에 함수를 주입합니다.
class A(){
private readonly IB _b;
public A(IB b){
_b = b;
}
void ExecuteTheFlow(){
_b.A();
_b.B();
_b.C();
}
}
3. 함수 홀더 클래스의 함수는 그들 사이에 종속성이 없어야 합니다.
void Func1(){
var list = GetList();
}
해야한다
void Func1(List<..> list){
}
다음 예에서는 장치를 보다 쉽게 테스트할 수 있도록 코드를 설계하는 방법을 설명합니다.
모든 예제는 C# 언어와 XUnit 테스트 프레임워크로 생성됩니다.
다음은 테스트하려는 클래스입니다. 인터페이스 IDependency는 종속성 주입의 예로 주입되었습니다.
public class ClassToTest
{
private readonly IDependency _dependency;
public ClassToTest(IDependency dependency)
{
_dependency = dependency;
}
public int? GetResult(int a)
{
var result = _dependency.ReturnValue(a);
var subtracted = Subtract(result);
if (subtracted < 2)
{
_dependency.SomeFunctionA();
}
else
{
_dependency.SomeFunctionB();
}
return subtracted;
}
public int? Subtract(int? value)
{
if (value < 4)
{
return value - 1;
}
return null;
}
}
GetResult 함수가 4보다 작은 값을 받은 경우 값에서 1을 뺀 값이 null을 반환하도록 하고 싶습니다. 그리고 뺄셈 후 값이 2보다 작으면 함수 A를 호출하고 그렇지 않으면 함수 B를 호출합니다.
이를 확인하는 테스트는 다음과 같습니다.
public class TestClassTests
{
[Fact]
public void Sutructed_1_IfTheValueIsLowerThan4()
{
var value = 1;
var expected = 0;
var dependencyA = new Mock<IDependency>();
dependencyA.Setup(x => x.ReturnValue(It.IsAny<int>())).Returns(value);
var target = new ClassToTest(dependencyA.Object);
var result = target.GetResult(value);
Assert.Equal(expected, result);
}
[Theory]
[InlineData(4)]
[InlineData(5)]
public void Returned_Null_IfTheValueIsGreaterOrEqualTo4(int value)
{
int? expected = null;
var dependencyA = new Mock<IDependency>();
dependencyA.Setup(x => x.ReturnValue(It.IsAny<int>())).Returns(value);
var target = new ClassToTest(dependencyA.Object);
var result = target.GetResult(value);
Assert.Equal(expected, result);
}
[Fact]
public void EnsureTheFlow_SomeFunctionA_CalledIfReturnedValueLowerThan2()
{
var dependency = new Mock<IDependency>();
dependency.Setup(x => x.ReturnValue(It.IsAny<int>())).Returns(2);
var target = new ClassToTest(dependency.Object);
var result = target.GetResult(It.IsAny<int>());
dependency.Verify(x => x.SomeFunctionA(), Times.Once());
}
[Theory]
[InlineData(3)]
[InlineData(4)]
public void EnsureTheFlow_SomeFunctionB_CalledIfSubtractedValueGreaterOrEqualTo2(int value)
{
var dependency = new Mock<IDependency>();
dependency.Setup(x => x.ReturnValue(It.IsAny<int>())).Returns(value);
var target = new ClassToTest(dependency.Object);
var result = target.GetResult(It.IsAny<int>());
dependency.Verify(x => x.SomeFunctionB(), Times.Once());
}
}
테스트가 작동하므로 예제의 테스트에 어떤 문제가 있습니까?
우리는 2가지 목표를 달성하기를 원합니다. 1. 쓰기와 2. 유지보수가 더 쉬운 단위 테스트입니다. 위 코드 디자인의 문제는 누군가가 리팩토링을 하고 실수로 if (value < 4)를 함수 Subtract에서 if (value > 4)로 바꾸면 문제가 어디에 있는지에 대한 명확한 표시 없이 일부 테스트가 실패한다는 것입니다. 그 순간 개발자는 단위 테스트가 실패한 이유를 찾기 위해 코드 디버그를 시작합니다. 60/60 법칙을 기억하십시오. 소프트웨어 시스템의 수명 주기 비용의 60%는 유지 관리에서 발생합니다(평균). 여기서 우리는 유지 관리가 더 어려운 코드를 생성하면서 규칙을 엉망으로 만들기 시작합니다. 따라서 유지 관리 비용은 문제를 찾는 개발자의 신경과 테스트가 실패한 이유에 비례하여 증가합니다.
코드는 일반적으로 훨씬 더 복잡하고 함수는 함수를 호출하는 함수를 호출합니다... 한 번의 회귀는 수십 개의 테스트에 실패할 수 있으며 그 중 하나가 문제의 원인을 명확하게 지적하지 않습니다.
이것을 개선합시다. 아래에서 테스트할 클래스의 예를 살펴보십시오.
public class ClassToTest
{
private readonly IDependency _dependency;
public ClassToTest(IDependency dependency)
{
_dependency = dependency;
}
public int? GetResult(int a)
{
var result = _dependency.ReturnValue(a);
var subtracted = _dependency.Subtract(result);
if(subtracted < 2)
{
_dependency.SomeFunctionA();
}
else
{
_dependency.SomeFunctionB();
}
return subtracted;
}
}
테스트는 다음과 같습니다.
public class TestClassTestsRight
{
[Fact]
public void Sutructed_1_IfTheValueIsLowerThan4()
{
var expected = 0;
var value = 1;
var target = new Dependency();
var actual = target.Subtract(value);
Assert.Equal(expected, actual);
}
[Theory]
[InlineData(4)]
[InlineData(5)]
public void Returned_Null_IfTheValueIsGreaterOrEqualTo4(int value)
{
var target = new Dependency();
var actual = target.Subtract(value);
Assert.Null(actual);
}
[Fact]
public void EnsureTheFlow_SomeFunctionA_CalledIfReturnedValueLowerThan2()
{
var dependency = new Mock<IDependency>();
dependency.Setup(x => x.ReturnValue(It.IsAny<int>())).Returns(1);
dependency.Setup(x => x.Subtract(It.IsAny<int>())).Returns(1);
var target = new ClassToTest(dependency.Object);
var result = target.GetResult(It.IsAny<int>());
dependency.Verify(x => x.SomeFunctionA(), Times.Once());
}
[Theory]
[InlineData(2)]
[InlineData(3)]
public void EnsureTheFlow_SomeFunctionB_CalledIfReturnedValueGreaterOrEqualTo2(int value)
{
var dependency = new Mock<IDependency>();
dependency.Setup(x => x.ReturnValue(It.IsAny<int>())).Returns(value);
dependency.Setup(x => x.Subtract(It.IsAny<int>())).Returns(value);
var target = new ClassToTest(dependency.Object);
var result = target.GetResult(It.IsAny<int>());
dependency.Verify(x => x.SomeFunctionB(), Times.Once());
}
}
빼기 함수는 종속성 클래스로 이동하여 별도로 테스트했습니다. GetResult 함수의 흐름도 별도로 테스트했습니다. 누군가가 if(value < 4)를 if(value > 4)로 바꾸면 특정 기능과 관련된 테스트는 실패하지만 흐름에 회귀가 없으므로 흐름 테스트는 성공합니다. 개발자는 어디에 문제가 있고 왜 실패하는지 알 수 있습니다. GetResult 함수에서 무언가가 변경되면 해당 함수와 관련된 테스트만 실패하고 흐름 문제를 가리킵니다.
https://github.com/genichm/unit-tests-example 에서 예제를 다운로드할 수 있습니다. .NET Core 3.1로 생성되었으며 XUnit 테스트 프레임워크를 사용합니다.
Reference
이 문제에 관하여(단위 테스트를 더 쉽고 쉽게 유지 관리할 수 있는 코드를 설계하는 방법에 대한 실용적인 팁), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://dev.to/genichm/practical-tips-on-how-to-design-code-that-makes-unit-testing-easier-and-more-easily-maintainable-1omf텍스트를 자유롭게 공유하거나 복사할 수 있습니다.하지만 이 문서의 URL은 참조 URL로 남겨 두십시오.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)