Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Parameterized tests on type-safe manner #19

Closed
jwChung opened this issue Mar 24, 2014 · 2 comments
Closed

Parameterized tests on type-safe manner #19

jwChung opened this issue Mar 24, 2014 · 2 comments

Comments

@jwChung
Copy link
Owner

jwChung commented Mar 24, 2014

우리는 종종 아래와 같이 하나의 테스트에서 여러 테스트를 동시에 실행하는 것으로 Parameterized test 효과를 누리고자 한다. 이러한 테스트를 xUnit Patterns에서는 Tabular Test라 한다.

[Fact]
public void AddTest()
{
    AddTestCase(1, 2, 3);
    AddTestCase(2, 3, 5);
    AddTestCase(10, 2, 12);
}

public void AddTestCase(int a, int b, int expected)
{
    // Fixture setup
    var sut = new Calc();

    // Exercise system
    var actual = sut.Add(a, b);

    // Verify outcome
    Assert.Equal(expected, actual);
}

하지만 위와 같은 AddTest는 3개의 테스트가 아니라 하나의 테스트로 작동하기 때문에 모든 테스트가 통과할 때는 문제가 없지만, 만약 하나라도 실패하게 된다면 우리는 그 실패가 3가지 중 어느 테스트에서 발생하였는지 쉽게 알아차릴 수 없다. (Eager Test)

이 Eager Test 문제를 해결하기 위해서 Experiment에서는 아래와 같이 어트리뷰트를 사용하여 각각의 테스트를 분리하였다. (이를 앞선 Tabular Test와 비교하기 위해 Attribute Tabular Test라 칭하자) 이 경우 AddTest는 하나의 테스트가 아니라 인수 별로 3개의 테스트로 작동하게 된다.

[NaiveTheorem]
[InlineData(1, 2, 3)]
[InlineData(2, 3, 5)]
[InlineData(10, 2, 12)]
public void AddTest(int a, int b, int expected)
{
    // Fixture setup
    var sut = new Calc();

    // Exercise system
    var actual = sut.Add(a, b);

    // Verify outcome
    Assert.Equal(expected, actual);
}

하지만 Attribute Tabular Test는 Tabular Test에는 없는 문제점이 하나 있다. type-safe 매너가 아니라는 점이다. 만약 int Add(int, int)decimal Add(decimal, decimal)로 시그니처를 변경하면, Tabular Test의 경우는 IDE에서 컴파일 애러를 내거나, 리팩토링할 수 있는 기회를 제공하지만, Attribute Tabular Test는 아래와 같이 이 경우 컴파일 애러가 발생하지 않는다.

[NaiveTheorem]
[InlineData(1, 2, 3)]
[InlineData(2, 3, 5)]
[InlineData(10, 2, 12)]
public void AddTest(decimal a, decimal b, decimal expected)
{
    // Fixture setup
    var sut = new Calc();

    // Exercise system
    var actual = sut.Add(a, b);

    // Verify outcome
    Assert.Equal(expected, actual);
}

더욱이 이 경우 decimal은 상수가 아니므로 InlineData(1, 2, 3)와 같이 inline으로 바로 attribute에 값을 작성할 수 없고 아래 AddDataAttribute와 같이 또 다른 attribute class가 필요하여 자칫 잘못하면 코드가 난잡해질 수 있다(verbose and complex).

[NaiveTheorem]
[AddData]
public void AddTest(decimal a, decimal b, decimal expected)
{
    // Fixture setup
    var sut = new Calc();

    // Exercise system
    var actual = sut.Add(a, b);

    // Verify outcome
    Assert.Equal(expected, actual);
}

private class AddDataAttribute : DataAttribute
{
    public override IEnumerable<object[]> GetData(MethodInfo methodUnderTest, Type[] parameterTypes)
    {
        yield return new object[] { 1M, 2M, 3M };
        yield return new object[] { 2M, 3M, 5M };
        yield return new object[] { 10M, 2M, 12M };
    }
}

그렇다면, Tabular Test와 Attribute Tabular Test의 장점만을 살릴 수 있는 방안은 없을까?

@jwChung
Copy link
Owner Author

jwChung commented Mar 24, 2014

사실 위의 질문은 Bug squash의 First-class tests in MbUnit라는 포스트에서 영감을 얻은 것이다. 이 포스트에서는 아래와 같은 방법을 제시하고 있다.

[StaticTestFactory]
public static IEnumerable<Test> Tests()
{
    var parameters = new[]
    {
        new { a = 1M, b = 2M, expected = 3M },
        new { a = 2M, b = 3M, expected = 5M },
        new { a = 10M, b = 2M, expected = 12M }
    };
    return parameters.Select(p => new TestCase(() =>
    {
        var sut = new Calc();
        var actual = sut.Add(p.a, p.b);
        Assert.Equal(p.expected, actual);
    }));
}

또한 Exude라는 프로젝트가 있는데 위와 유사한 방법으로 문제를 해결하고 있다.

@jwChung
Copy link
Owner Author

jwChung commented Mar 25, 2014

Experiment에서는 앞서 Bug squash의 approach는 애러가 난다. Experiment의 TestCase.New(Delegate)에서 Delegate는 static method에 관한 것이어야 한다. 즉 TestCase.New 의 델리게이트에서 바깥 범위의 객체로 접근해서는 안된다. 왜냐하면 아래와 같은 문제가 발생할 수 있기 때문이다.

  • Erratic Tests
  • probably complicated than Minimal Fixture
  • leading to Fragile Fixture.

자세한 내용은 xUnit Patterns를 참고하세요.

아래코드는 p 변수 사용으로 문제가 발생한다.

return parameters.Select(p => TestCase.New(() =>
{
    var sut = new Calc();
    var actual = sut.Add(p.a, p.b);
    Assert.Equal(p.expected, actual);
}));

Experiment에서 이를 해결하기 위해 p를 TestCase.New의 argument로 넘겨주어야 한다.

return parameters.Select(p => TestCase.New(p, p2 =>
{
    var sut = new Calc();
    var actual = sut.Add(p2.a, p2.b);
    Assert.Equal(p2.expected, actual);
}));

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant