단위 테스트를 더 쉽고 쉽게 유지 관리할 수 있는 코드를 설계하는 방법에 대한 실용적인 팁

단위 테스트를 작성하고 유지하는 것은 놀라울 정도로 쉽습니다. 다음 작업만 하면 됩니다.
  • 종속성 주입 사용
  • 함수 홀더 클래스에서 함수 실행기를 분리함
  • 함수 홀더 클래스의 함수 간에 종속성이 없어야 함

  • 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 테스트 프레임워크를 사용합니다.

    좋은 웹페이지 즐겨찾기