Nice programing

단위 테스트는 왜 한 가지만 테스트해야합니까?

nicepro 2020. 12. 13. 11:05
반응형

단위 테스트는 왜 한 가지만 테스트해야합니까?


좋은 단위 테스트는 무엇입니까? 테스트는 한 가지만 테스트해야한다고 말합니다. 그것의 이점은 무엇입니까?

더 큰 코드 블록을 테스트하는 좀 더 큰 테스트를 작성하는 것이 낫지 않을까요? 테스트 실패를 조사하는 것은 어쨌든 어렵고 작은 테스트에서는 도움이되지 않습니다.

편집 : 단위라는 단어는 그다지 중요하지 않습니다. 단위가 조금 더 크다고 가정 해 봅시다. 그것은 여기서 문제가 아닙니다. 진짜 질문은 많은 방법을 다루는 몇 가지 테스트가 더 간단하기 때문에 모든 방법에 대해 테스트 이상을 만드는 이유입니다.

예 : 목록 클래스. 추가 및 제거를 위해 별도의 테스트를해야하는 이유는 무엇입니까? 먼저 추가 한 다음 사운드를 제거하는 하나의 테스트가 더 간단합니다.


나는 여기서 사지로 나가서 "하나만 테스트하라"는 충고는 때때로 만들어지는 것처럼 실제로 도움이되지 않는다고 말할 것입니다.

때로는 테스트에 일정량의 설정이 필요합니다. 때때로 그들은 심지어 어느 정도 걸릴 수 있습니다 시간 (현실 세계에서) 설정합니다. 종종 한 번에 두 가지 작업을 테스트 할 수 있습니다.

장점 : 모든 설정이 한 번만 발생하도록합니다. 첫 번째 행동 이후의 테스트는 세상이 두 번째 행동 전에 예상되는 방식임을 증명할 것입니다. 더 적은 코드, 더 빠른 테스트 실행.

단점 : 작업 중 하나 가 실패하면 동일한 결과를 얻을 수 있습니다. 동일한 테스트가 실패합니다. 두 테스트에서 각각 하나의 작업 만 수행 한 경우보다 문제의 위치에 대한 정보가 적습니다.

실제로 나는 여기에있는 "단점"이 큰 문제가 아니라는 것을 알았다. 스택 추적은 종종 매우 빠르게 범위를 좁히고 어쨌든 코드를 수정하도록하겠습니다.

여기서 약간 다른 "단점"은 "새 테스트 작성, 통과, 리팩터링"주기를 위반한다는 것입니다. 나는 그것을 이상적인 순환이라고 생각하지만 항상 현실을 반영하지는 않습니다. 때로는 새로운 작업을 생성하는 것보다 현재 테스트에서 추가 작업을 추가하고 확인 (또는 기존 작업에 대한 또 다른 확인)하는 것이 더 실용적입니다.


한 가지만 테스트하면 그 한 가지를 격리하고 작동 여부를 증명할 수 있습니다. 이것이 단위 테스트의 아이디어입니다. 둘 이상의 것을 테스트하는 테스트에는 문제가 없지만 일반적으로 통합 테스트라고합니다. 둘 다 상황에 따라 장점이 있습니다.

예를 들어, 침대 옆 램프가 켜지지 않고 전구를 교체하고 연장 코드를 바꾸면 어떤 변경으로 문제가 해결되었는지 알 수 없습니다. 단위 테스트를 수행하고 문제를 분리하기 위해 우려 사항을 분리해야합니다.


둘 이상의 것을 확인하는 테스트는 더 밀접하게 결합되고 부서지기 때문에 일반적으로 권장되지 않습니다. 코드에서 무언가를 변경하면 고려해야 할 것이 더 많기 때문에 테스트를 변경하는 데 시간이 더 오래 걸립니다.

[편집 :] 좋습니다, 이것이 샘플 테스트 방법이라고 말하십시오 :

[TestMethod]
public void TestSomething() {
  // Test condition A
  // Test condition B
  // Test condition C
  // Test condition D
}

조건 A에 대한 테스트가 실패하면 B, C 및 D도 실패한 것처럼 보이며 유용성을 제공하지 않습니다. 코드 변경으로 인해 C도 실패했다면 어떨까요? 그것들을 4 개의 개별 테스트로 나눈다면 이것을 알 것입니다.


하아 ... 단위 테스트.

"지시"를 너무 많이 밀면 빠르게 사용할 수 없게됩니다.

단일 단위 테스트 테스트는 단일 방법이 단일 작업을 수행하는 것처럼 좋은 방법입니다. 그러나 단일 테스트를 의미하지 않는 IMHO는 단일 assert 문만 포함 할 수 있습니다.

이다

@Test
public void checkNullInputFirstArgument(){...}
@Test
public void checkNullInputSecondArgument(){...}
@Test
public void checkOverInputFirstArgument(){...}
...

보다 낫다

@Test
public void testLimitConditions(){...}

제 생각에는 좋은 습관 이라기보다는 취향의 문제입니다. 나는 개인적으로 후자를 훨씬 선호합니다.

그러나

@Test
public void doesWork(){...}

실제로 "지시문"이 당신이 어떤 대가를 치르더라도 피하길 바라는 것이고 내 정신을 가장 빨리 소모시키는 것입니다.

마지막 결론으로, 의미 상 관련이 있고 쉽게 테스트 할 수있는 항목을 함께 그룹화하여 실패한 테스트 메시지 자체가 실제로 코드로 직접 이동하기에 충분히 의미가 있도록합니다.

실패한 테스트 보고서에 대한 경험 법칙 : 먼저 테스트 코드를 읽어야한다면 테스트가 충분히 구조화되지 않았고 더 작은 테스트로 더 분할해야합니다.

내 2 센트.


자동차를 만드는 것을 생각해보십시오. 당신의 이론을 적용한다면, 단지 큰 것들을 시험하는 것이라면, 사막을 통과하는 차를 운전하는 시험을하는 것은 어떨까요? 무너집니다. 좋습니다. 문제의 원인을 알려주세요. 당신은 할 수 없습니다. 그것은 시나리오 테스트입니다.

기능 테스트는 엔진을 켜는 것일 수 있습니다. 실패합니다. 그러나 그것은 여러 가지 이유 때문일 수 있습니다. 여전히 문제의 원인을 정확히 말할 수 없었습니다. 우리는 점점 가까워지고 있습니다.

단위 테스트는 더 구체적이며 먼저 코드가 손상된 위치를 식별하지만 (적절한 TDD를 수행하는 경우) 코드를 명확한 모듈 식 청크로 설계하는 데 도움이됩니다.

누군가 스택 추적 사용에 대해 언급했습니다. 잊어 버려. 그것은 두 번째 수단입니다. 스택 추적을 진행하거나 디버그를 사용하는 것은 고통스럽고 시간이 많이 걸릴 수 있습니다. 특히 더 큰 시스템과 복잡한 버그에서.

단위 테스트의 좋은 특성 :

  • 빠름 (밀리 초)
  • 독립적 인. 다른 테스트의 영향을 받거나 의존하지 않습니다.
  • 맑은. 부풀거나 많은 양의 설정을 포함해서는 안됩니다.

테스트 기반 개발을 사용하면 먼저 테스트를 작성한 다음 테스트를 통과하는 코드를 작성합니다. 테스트에 중점을두면 테스트를 통과하는 코드를 더 쉽게 작성할 수 있습니다.

예를 들어 매개 변수를받는 메소드가있을 수 있습니다. 내가 가장 먼저 생각할 수있는 것 중 하나는 매개 변수가 null이면 어떻게됩니까? ArgumentNull 예외가 발생해야합니다. 그래서 저는 null 인수를 전달할 때 예외가 발생하는지 확인하는 테스트를 작성합니다. 테스트를 실행하십시오. 좋아, NotImplementedException이 발생합니다. ArgumentNull 예외를 throw하도록 코드를 변경하여 수정합니다. 내 테스트를 실행하십시오. 그렇다면 너무 작거나 너무 크면 어떻게 될까요? 아, 두 가지 테스트입니다. 먼저 너무 작은 경우를 씁니다.

요점은 한 번에 방법의 동작을 생각하지 않는다는 것입니다. 무엇을해야하는지 생각함으로써 점진적으로 (그리고 논리적으로) 빌드 한 다음 코드를 구현하고 예쁘게 보이도록 리팩토링합니다. 행동에 대해 생각할 때 작고 이해할 수있는 단위로 개발해야하기 때문에 테스트가 작고 집중되어야하는 이유입니다.


한 가지만 확인하는 테스트가 있으면 문제 해결이 더 쉬워집니다. 여러 가지를 테스트하는 테스트 나 동일한 설정 / 해체를 공유하는 여러 테스트가 있어서는 안된다는 말은 아닙니다.

여기에 예시가 있어야합니다. 쿼리가있는 스택 클래스가 있다고 가정 해 보겠습니다.

  • getSize
  • 비었다
  • getTop

및 스택을 변경하는 방법

  • push (anObject)
  • 팝()

이제 다음 테스트 케이스를 고려하십시오 (이 예제에서는 의사 코드와 같은 Python을 사용하고 있습니다.).

class TestCase():
    def setup():
        self.stack = new Stack()
    def test():
        stack.push(1)
        stack.push(2)
        stack.pop()
        assert stack.top() == 1, "top() isn't showing correct object"
        assert stack.getSize() == 1, "getSize() call failed"

이 테스트 케이스에서 문제가 있는지 확인할 수 있지만, push()또는 pop()구현 또는 값을 반환하는 쿼리에 격리되었는지 여부는 확인할 수 없습니다 . top()getSize().

각 방법과 그 동작에 대한 개별 테스트 케이스를 추가하면 상황을 진단하기가 훨씬 쉬워집니다. 또한 각 테스트 사례에 대해 새로운 설정을 수행함으로써 실패한 테스트 메서드가 호출 한 메서드 내에 문제가 완전히 포함되도록 보장 할 수 있습니다.

def test_size():
    assert stack.getSize() == 0
    assert stack.isEmpty()

def test_push():
    self.stack.push(1)
    assert stack.top() == 1, "top returns wrong object after push"
    assert stack.getSize() == 1, "getSize wrong after push"

def test_pop():
    stack.push(1)
    stack.pop()
    assert stack.getSize() == 0, "getSize wrong after push"

테스트 주도 개발에 관한 한. 개인적으로 처음에는 여러 메서드를 테스트하는 더 큰 "기능 테스트"를 작성한 다음 개별 부분을 구현하기 시작할 때 단위 테스트를 만듭니다.

그것을 보는 또 다른 방법은 단위 테스트가 각 개별 메서드의 계약을 확인하는 반면, 더 큰 테스트는 개체와 시스템 전체가 따라야하는 계약을 확인하는 것입니다.

나는 아직도 세 메서드 호출을 사용하고 test_push있지만 모두 top()getSize()별도의 시험 방법에 의해 테스트 쿼리입니다.

단일 테스트에 더 많은 어설 션을 추가하여 유사한 기능을 얻을 수 있지만 나중에 어설 션 실패가 숨겨집니다.


둘 이상의 것을 테스트하는 경우 단위 테스트가 아닌 통합 테스트라고합니다. 단위 테스트와 동일한 테스트 프레임 워크에서 이러한 통합 테스트를 계속 실행합니다.

통합 테스트는 일반적으로 느리고 단위 테스트는 모든 종속성이 모의 / 가짜이므로 빠르므로 데이터베이스 / 웹 서비스 / 느린 서비스 호출이 없습니다.

우리는 소스 제어 커밋에 대해 단위 테스트를 실행하고 통합 테스트는 야간 빌드에서만 실행됩니다.


더 작은 단위 테스트는 실패했을 때 문제가 어디에 있는지 더 명확하게합니다.


GLib이지만 여전히 유용하기를 바랍니다. 대답은 unit = 1입니다. 둘 이상의 것을 테스트하는 경우 단위 테스트가 아닙니다.


둘 이상의 것을 테스트하고 첫 번째 테스트가 실패하면 테스트하는 후속 항목이 합격인지 실패하는지 알 수 없습니다. 실패 할 모든 것을 알고 있으면 고치는 것이 더 쉽습니다.


귀하의 예와 관련하여 : 동일한 단위 테스트에서 추가 및 제거를 테스트하는 경우 항목이 목록에 추가되었는지 어떻게 확인합니까? 그렇기 때문에 하나의 테스트에서 추가되었는지 확인해야합니다.

또는 램프 예제를 사용하려면 : 램프를 테스트하고 싶은데 스위치를 켰다가 끄면 램프가 켜져 있는지 어떻게 알 수 있습니까? 램프를보고 램프가 켜져 있는지 확인하려면 중간 단계를 거쳐야합니다. 그런 다음 전원을 끄고 꺼져 있는지 확인할 수 있습니다.


단위 테스트는 한 가지만 테스트해야한다는 생각을지지합니다. 나는 또한 그것에서 상당히 벗어났다. 오늘은 값 비싼 설정으로 인해 테스트 당 하나 이상의 주장을해야하는 것처럼 보이는 테스트가있었습니다.

namespace Tests.Integration
{
  [TestFixture]
  public class FeeMessageTest
  {
    [Test]
    public void ShouldHaveCorrectValues
    {
      var fees = CallSlowRunningFeeService();
      Assert.AreEqual(6.50m, fees.ConvenienceFee);
      Assert.AreEqual(2.95m, fees.CreditCardFee);
      Assert.AreEqual(59.95m, fees.ChangeFee);
    }
  }
}

동시에 첫 번째 주장뿐만 아니라 실패한 모든 주장을보고 싶었습니다. 나는 그들 모두가 실패 할 것으로 예상하고 있었고, 내가 실제로 얼마나 많은 돈을 받고 있는지 알아야했습니다. 그러나 각 테스트가 분할 된 표준 [SetUp]은 느린 서비스에 대해 3 번의 호출을 유발합니다. 갑자기 나는 "비 전통적인"테스트 구조를 사용하는 것이 단위 테스트의 장점의 절반이 숨겨져 있다는 것을 제안하는 기사를 떠올 렸습니다. (제레미 밀러 게시물 인 것 같지만 지금은 찾을 수 없습니다.) 갑자기 [TestFixtureSetUp]이 떠 올랐고 저는 단일 서비스 호출을 할 수 있지만 여전히 별도의 표현적인 테스트 방법이 있다는 것을 깨달았습니다.

namespace Tests.Integration
{
  [TestFixture]
  public class FeeMessageTest
  {
    Fees fees;
    [TestFixtureSetUp]
    public void FetchFeesMessageFromService()
    {
      fees = CallSlowRunningFeeService();
    }

    [Test]
    public void ShouldHaveCorrectConvenienceFee()
    {
      Assert.AreEqual(6.50m, fees.ConvenienceFee);
    }

    [Test]
    public void ShouldHaveCorrectCreditCardFee()
    {
      Assert.AreEqual(2.95m, fees.CreditCardFee);
    }

    [Test]
    public void ShouldHaveCorrectChangeFee()
    {
      Assert.AreEqual(59.95m, fees.ChangeFee);
    }
  }
}

이 테스트에는 더 많은 코드가 있지만 기대와 일치하지 않는 모든 값을 한 번에 보여줌으로써 훨씬 더 많은 가치를 제공합니다.

A colleague also pointed out that this is a bit like Scott Bellware's specunit.net: http://code.google.com/p/specunit-net/


Another practical disadvantage of very granular unit testing is that it breaks the DRY principle. I have worked on projects where the rule was that each public method of a class had to have a unit test (a [TestMethod]). Obviously this added some overhead every time you created a public method but the real problem was that it added some "friction" to refactoring.

It's similar to method level documentation, it's nice to have but it's another thing that has to be maintained and it makes changing a method signature or name a little more cumbersome and slows down "floss refactoring" (as described in "Refactoring Tools: Fitness for Purpose" by Emerson Murphy-Hill and Andrew P. Black. PDF, 1.3 MB).

Like most things in design, there is a trade-off that the phrase "a test should test only one thing" doesn't capture.


When a test fails, there are three options:

  1. The implementation is broken and should be fixed.
  2. The test is broken and should be fixed.
  3. The test is not anymore needed and should be removed.

Fine-grained tests with descriptive names help the reader to know why the test was written, which in turn makes it easier to know which of the above options to choose. The name of the test should describe the behaviour which is being specified by the test - and only one behaviour per test - so that just by reading the names of the tests the reader will know what the system does. See this article for more information.

On the other hand, if one test is doing lots of different things and it has a non-descriptive name (such as tests named after methods in the implementation), then it will be very hard to find out the motivation behind the test, and it will be hard to know when and how to change the test.

Here is what a it can look like (with GoSpec), when each test tests only one thing:

func StackSpec(c gospec.Context) {
  stack := NewStack()

  c.Specify("An empty stack", func() {

    c.Specify("is empty", func() {
      c.Then(stack).Should.Be(stack.Empty())
    })
    c.Specify("After a push, the stack is no longer empty", func() {
      stack.Push("foo")
      c.Then(stack).ShouldNot.Be(stack.Empty())
    })
  })

  c.Specify("When objects have been pushed onto a stack", func() {
    stack.Push("one")
    stack.Push("two")

    c.Specify("the object pushed last is popped first", func() {
      x := stack.Pop()
      c.Then(x).Should.Equal("two")
    })
    c.Specify("the object pushed first is popped last", func() {
      stack.Pop()
      x := stack.Pop()
      c.Then(x).Should.Equal("one")
    })
    c.Specify("After popping all objects, the stack is empty", func() {
      stack.Pop()
      stack.Pop()
      c.Then(stack).Should.Be(stack.Empty())
    })
  })
}

The real question is why make a test or more for all methods as few tests that cover many methods is simpler.

Well, so that when some test fails you know which method fails.

When you have to repair a non-functioning car, it is easier when you know which part of the engine is failing.

An example: A list class. Why should I make separate tests for addition and removal? A one test that first adds then removes sounds simpler.

Let's suppose that the addition method is broken and does not add, and that the removal method is broken and does not remove. Your test would check that the list, after addition and removal, has the same size as initially. Your test would be in success. Although both of your methods would be broken.


Disclaimer: This is an answer highly influenced by the book "xUnit Test Patterns".

Testing only one thing at each test is one of the most basic principles that provides the following benefits:

  • Defect Localization: If a test fails, you immediately know why it failed (ideally without further troubleshooting, if you've done a good job with the assertions used).
  • Test as a specification: the tests are not only there as a safety net, but can easily be used as specification/documentation. For instance, a developer should be able to read the unit tests of a single component and understand the API/contract of it, without needing to read the implementation (leveraging the benefit of encapsulation).
  • Infeasibility of TDD: TDD is based on having small-sized chunks of functionality and completing progressive iterations of (write failing test, write code, verify test succeeds). This process get highly disrupted if a test has to verify multiple things.
  • Lack of side-effects: Somewhat related to the first one, but when a test verifies multiple things, it's more possible that it will be tied to other tests as well. So, these tests might need to have a shared test fixture, which means that one will be affected by the other one. So, eventually you might have a test failing, but in reality another test is the one that caused the failure, e.g. by changing the fixture data.

I can only see a single reason why you might benefit from having a test that verifies multiple things, but this should be seen as a code smell actually:

  • Performance optimisation: There are some cases, where your tests are not running only in memory, but are also dependent in persistent storage (e.g. databases). In some of these cases, having a test verify multiple things might help in decreasing the number of disk accesses, thus decreasing the execution time. However, unit tests should ideally be executable only in memory, so if you stumble upon such a case, you should re-consider whether you are going in the wrong path. All persistent dependencies should be replaced with mock objects in unit tests. End-to-end functionality should be covered by a different suite of integration tests. In this way, you do not need to care about execution time anymore, since integration tests are usually executed by build pipelines and not by developers, so a slightly higher execution time has almost no impact to the efficiency of the software development lifecycle.

참고URL : https://stackoverflow.com/questions/235025/why-should-unit-tests-test-only-one-thing

반응형