[번역]JUnit A Cook’s Tour

예전에 토비의 스프링의 저자 이일민님에게 뜬금없이 아래와 같은 질문을 한 적이 있습니다.

스프링 프레임워크 소스를 분석하면서 객체지향 공부를 하려고 하는데 어떻게 해야 할까요?

이에 대한 답으로 JUnit A Cook’s Tour를 읽어보라고 하셔서 공부할 겸 번역해 보았습니다.

오역이 있음을 감안하시고, 번역하기에 애매한 부분은 원문 그대로 옮겼습니다.


Note : 이 기사는 Junit 3.8.x를 기반으로 한다.


1. Introduction

이전 기사에서 우리는 간단한 프레임워크를 사용해 반복가능한 테스트를 만드는 방법에 대해 설명했다.
이번 기사에서 우리는 프레임워크를 살펴보고 그것이 어떻게 구성되었는지 보여준다.

우리는 JUnit 프레임워크를 주의깊게 연구하고 우리가 그것을 어떻게 구성했는지를 반영했다.
우리는 다양한 수준에서 교훈을 발견했다. 이 기사에서 우리는 한 번에 모든 것을 전달하려고
노력하겠지만, 적어도 검증된 가치를 지닌 소프트웨어의 설계 및 구성을 보여주는 맥락에서
이를 진행할 것이다.

우리는 프레임 워크의 목표에 대한 토론으로 시작한다. 목표는 프레임워크 자체가 보여지는 동안 많은 작은 세부 사항에서 다시 나타난다. 이후, 우리는 프레임워크의 설계와 구현을 제시한다.
설계는 세련된프로그램으로 구현된 패턴(놀람, 놀라움)의 관점에서 설명 될 것이다.
우리는 프레임워크 개발에 대한 몇 가지 선택적인 생각으로 결론을 맺는다.

2. Goals

Junit의 목표는 무엇인가?

첫째,  개발시 다음과 같은 가정을 해보자. 프로그램 기능에 자동화 된 테스트가없는 경우 작동하지 않는다고 가정한다. 개발자가 우리에게 프로그램 기능이 작동한다는 것을 보증한다면 현재와 영원히 작동한다는 일반적인 가정보다 훨씬 안전하다.

이 관점에서 개발자는 코드를 만들거나 디버깅할 때 이것이 이뤄지지 않으면, 프로그램이 동작함을 입증하는 테스트들을 만들어야 한다. 그럼에도 불구하고, 모두는 바쁘고, 그들은 할일이 많고,  충분한 시간을 할애하지 못한 채 테스트를 하며 보낸다. 난 이미 너무 많은 코드를 작성했는데 어떻게 또 테스트코드를 만들까요? 대답해주세요. Mr. Hard-case Project Manager.

그래서 목표 중 하나는 개발자들이 실제로 테스트코드를 만들 희망이 조금 있는 프레임워크를 만드는 것이다. 프레임워크는 친숙한 도구를 사용하므로 새로 익힐 것이 거의 없다. 그것은 테스트를 만드는 데 절대적으로 필요한 것 이상의 작업을 요구하면 안된다. 그것은 중복된 노력을 제거해준다.

모든 테스트들이 동작해야 할 경우, 디버거에 표현식을 작성하면 된다. 그러나 이것은 테스트를 하기에 충분하지 않다. 당신의 프로그램 작동이 나에게 도움이 되지 않는다. 왜냐하면 그것은 통합이 끝난 뒤 1분 후에도 프로그램이 정상작동하리라고 보증하지 않기 때문이다. 그리고 그것은 당신이 오랫동안 자리를 비웠을 때 5년뒤에도 정상적으로 동작한다고 보증하지 못한다.

그래서 테스트의 두번째 목표는 시간이 지나도 가치를 유지하는 테스트를 만드는 것이다. 때때로 테스트를 만든 사람이 아닌 다른 사람이 테스트를 실행하고 결과를 해석할 수 있어야 한다.  다양한 개발자들의 테스트를 결합하고 간섭의 두려움없이 그것들을 함께 실행할 수 있어야 한다.

마지막으로,  기존 테스트를 활용하여 새로운 테스트를 만들 수 있어야 한다. setup이나 fixture를 만드는 것은 값비싼 비용이 들고 프레임워크는 다른 테스트들을 실행하기 위해 fixture들을 재사용할 수 있어야 한다. 오, 그게 다야?

3. The Design of JUnit

JUnit의 디자인은 (“패턴 생성 아키텍처”, Kent Beck 및 Ralph Johnson, ECOOP 94 참조)에서 처음 사용된 스타일로 제공된다.  아이디어는 시스템의 아키텍처를 가질 때까지 아무것도 시작하지 않고 하나하나씩 패턴을 적용하여 시스템 설계를 설명하는 것이다. 우리는 해결할 아키텍처 문제를 제시하고, 이를 해결하는 패턴을 요약한 다음 패턴이 JUnit에 어떻게 적용되었는지 보여줄 것이다.

3.1 Getting started- TestCase

먼저 기본개념인 TestCase를 표현할 객체를 만들어야 한다.
개발자는 종종 테스트 사례를 염두에 두지만 여러 가지 방법으로 테스트 사례를 실현한다.

  • 구문 출력
  • 디버거 표현식
  • 테스트 스크립트들

조작이 쉬운 테스트를 만들고 싶다면 객체를 만들어야 한다.
이는 은연중에 개발자가 테스트를 받아들이고 구체적으로 만들어, 시간이 지남에 따라 가치를 유지하는 테스트를 만드는 목표를 뒷받침한다.
동시에 객체 개발자는 객체 개발에 익숙해지기 때문에 객체로 테스트를 하는 결정은 테스트 작성을
더 매력적으로 만드는 (또는 적어도 덜 강요하는) 목표를 뒷받침한다.
커맨드 패턴 (see Gamma, E., et al. Design Patterns: Elements of Reusable Object-Oriented Software, Addison-Wesley, Reading, MA, 1995)은 우리의 필요를 아주 잘 맞춘다.
의도를 인용하면 “요청을 객체로 캡슐화하여 큐 또는 로그 요청 …”명령은 작업에 대한 객체를 만들고 “execute”메소드를 제공한다. 다음은 TestCase의 클래스 정의 코드입니다.

public abstract class TestCase implements Test {
    …

이 클래스는 상속을 통해 재사용 될 것으로 기대하기 때문에 “public abstract”이라고 선언한다.
지금은 Test 인터페이스를 구현한다는 사실을 무시하라. 현재 설계의 목적을 위해 TestCase를
하나의 class로 생각할 수 있다.
모든 TestCase는 이름으로 작성되므로 테스트가 실패하면 실패한 테스트를 식별 할 수 있다.

public abstract class TestCase implements Test {
    private final String fName;
    public TestCase(String name) {
        fName= name;
    }

    public abstract void run();
        …
}

JUnit의 발전을 설명하기 위해, 아키텍처의 스냅 샷을 보여주는 다이어그램을 사용한다.
우리가 사용하는 표기법은 간단하다. 연관된 패턴을 포함하는 음영 처리된 상자로 클래스에 주석을
붙인다. 패턴에서 클래스의 역할이 분명하면 패턴이름만 표시된다. 역할이 명확하지 않은 경우
음영 처리된 상자는이 클래스가 해당하는 참가자의 이름으로 보강된다.
이 표기법은 다이어그램의 혼란을 최소화하기 위해 (see Gamma, E., Applying Design Patterns in Java, in Java Gems, SIGS Reference Library, 1997)에 처음으로 나타났다.
그림 1은 TestCase에 적용된 표기법을 보여준다. 우리는 하나의 클래스를 다루고 있으므로
모호하지 않고 패턴이름만 보여진다.

%e1%84%89%e1%85%b3%e1%84%8f%e1%85%b3%e1%84%85%e1%85%b5%e1%86%ab%e1%84%89%e1%85%a3%e1%86%ba-2016-12-03-%e1%84%8b%e1%85%a9%e1%84%8c%e1%85%a5%e1%86%ab-10-20-22

3.2 Blanks to fill in- run()

The next problem to solve is giving the developer a convenient “place” to put their fixture code and their test code. The declaration of TestCase as abstract says that the developer is expected to reuse TestCase by subclassing. However, if all we could do was provide a superclass with one variable and no behavior, we wouldn’t be doing much to satisfy our first goal, making tests easier to write.

Fortunately, there is a common structure to all tests- they set up a test fixture, run some code against the fixture, check some results, and then clean up the fixture. This means that each test will run with a fresh fixture and the results of one test can’t influence the result of another. This supports the goal of maximizing the value of the tests.

Template Method addresses our problem quite nicely. Quoting from the intent, “Define the skeleton of an algorithm in an operation, deferring some steps to subclasses. Template Method lets subclasses redefine certain steps of an algorithm without changing the algorithm’s structure.” This is exactly right. We want the developer to be able to separately consider how to write the fixture (set up and tear down) code and how to write the testing code. The execution of this sequence, however, will remain the same for all tests, no matter how the fixture code is written or how the testing code is written.

Here is the template method:

public void run() {
    setUp();
    runTest();
    tearDown();
}The default implementations of these methods do nothing:

protected void runTest() {
}protected void setUp() {
}

protected void tearDown() {
}

Since setUp and tearDown are intended to be overridden but will be called by the framework we declare them as protected. The second snapshot of our tour is depicted in Figure 2.

Figure 2 TestCase.run() applies Template Method

3.3 Reporting results- TestResult

If a TestCase runs in a forest, does anyone care about the result? Sure- you run tests to make sure they run. After the test has run, you want a record, a summary of what did and didn’t work.

If tests had equal chances of succeeding or failing, or if we only ever ran one test, we could just set a flag in the TestCase object and go look at the flag when the test completed. However, tests are (intended to be) highly asymmetric- they usually work. Therefore, we only want to record the failures and a highly condensed summary of the successes.

The Smalltalk Best Practice Patterns (see Beck, K. Smalltalk Best Practice Patterns, Prentice Hall, 1996) has a pattern that is applicable. It is called Collecting Parameter. It suggests that when you need to collect results over several methods, you should add a parameter to the method and pass an object that will collect the results for you. We create a new object, TestResult, to collect the results of running tests.

public class TestResult extends Object {
    protected int fRunTests;    public TestResult() {
       fRunTests= 0;
    }
}

This simple version of TestResult only counts the number of tests run. To use it, we have to add a parameter to the TestCase.run() method and notify the TestResult that the test is running:

public void run(TestResult result) {
    result.startTest(this);
    setUp();
    runTest();
    tearDown();
}And the TestResult has to keep track of the number of tests run:

public synchronized void startTest(Test test) {
    fRunTests++;
}We declare the TestResult method startTest as synchronized so that a single TestResult can collect the results safely when the tests are run in different threads. Finally, we want to retain the simple external interface of TestCase, so we create a no-parameter version of run() that creates its own TestResult:

public TestResult run() {
    TestResult result= createResult();
    run(result);
    return result;
}protected TestResult createResult() {
    return new TestResult();
}

Figure 3 shows our next design snapshot.

Figure 3: TestResult applies Collecting Parameter

If tests always ran correctly, then we wouldn’t have to write them. Tests are interesting when they fail, especially if we didn’t expect them to fail. What’s more, tests can fail in ways that we expect, for example by computing an incorrect result, or they can fail in more spectacular ways, for example by writing outside the bounds of an array. No matter how the test fails we want to execute the following tests.

JUnit distinguishes between failures and errors. The possibility of a failure is anticipated and checked for with assertions. Errors are unanticipated problems like an ArrayIndexOutOfBoundsException. Failures are signaled with an AssertionFailedError error. To distinguish an unanticipated error from a failure, failures are caught in an extra catch clause (1). The second clause (2) catches all other exceptions and ensures that our test run continues..

public void run(TestResult result) {
    result.startTest(this);
    setUp();
    try {
        runTest();
    }
    catch (AssertionFailedError e) { //1
        result.addFailure(this, e);
    }    catch (Throwable e) { // 2
        result.addError(this, e);
    }
    finally {
        tearDown();
    }
}

An AssertionFailedError is triggered by the assert methods provided by TestCase. JUnit provides a set of assert methods for different purposes. Here is the simplest one:

protected void assertTrue(boolean condition) {
    if (!condition)
        throw new AssertionFailedError();
}The AssertionFailedError is not meant to be caught by the client (a testing method inside a TestCase) but inside the Template Method TestCase.run(). We therefore derive AssertionFailedError from Error.

public class AssertionFailedError extends Error {
    public AssertionFailedError () {}
}The methods to collect the errors in TestResult are shown below:

public synchronized void addError(Test test, Throwable t) {
    fErrors.addElement(new TestFailure(test, t));
}public synchronized void addFailure(Test test, Throwable t) {
    fFailures.addElement(new TestFailure(test, t));
}

TestFailure is a little framework internal helper class to bind together the failed test and the signaled exception for later reporting.

public class TestFailure extends Object {
    protected Test fFailedTest;
    protected Throwable fThrownException;
}The canonical form of collecting parameter requires us to pass the collecting parameter to each method. If we followed this advice, each of the testing methods would require a parameter for the TestResult. This results in a “pollution” of these method signatures. As a benevolent side effect of using exceptions to signal failures we can avoid this signature pollution. A test case method, or a helper method called from it, can throw an exception without having to know about the TestResult. As a refresher here is a sample test method from our MoneyTest suite. It illustrates how a testing method doesn’t have to know anything about a TestResult:

public void testMoneyEquals() {
    assertTrue(!f12CHF.equals(null));
    assertEquals(f12CHF, f12CHF);
    assertEquals(f12CHF, new Money(12, “CHF”));
    assertTrue(!f12CHF.equals(f14CHF));
}JUnit comes with different implementations of TestResult. The default implementation counts the number of failures and errors and collects the results. TextTestResult collects the results and presents them in a textual form. Finally, UITestResult is used by the graphical version of the JUnit Test Runner to update the graphical test status.

TestResult is an extension point of the framework. Clients can define their own custom TestResult classes, for example, an HTMLTestResult reports the results as an HTML document.

3.4 No stupid subclasses – TestCase again

We have applied Command to represent a test. Command relies on a single method like execute() (called run() in TestCase) to invoke it. This simple interface allows us to invoke different implementations of a command through the same interface.

We need an interface to generically run our tests. However, all test cases are implemented as different methods in the same class. This avoids the unnecessary proliferation of classes. A given test case class may implement many different methods, each defining a single test case. Each test case has a descriptive name like testMoneyEquals or testMoneyAdd. The test cases don’t conform to a simple command interface. Different instances of the same Command class need to be invoked with different methods. Therefore our next problem is make all the test cases look the same from the point of view of the invoker of the test.

Reviewing the problems addressed by available design patterns, the Adapter pattern springs to mind. Adapter has the following intent “Convert the interface of a class into another interface clients expect”. This sounds like a good match. Adapter tells us different ways to do this. One of them is a class adapter, which uses subclassing to adapt the interface. For example, to adapt testMoneyEquals to runTest we implement a subclass of MoneyTest and override runTest to invoke testMoneyEquals.

public class TestMoneyEquals extends MoneyTest {
    public TestMoneyEquals() { super(“testMoneyEquals”); }
    protected void runTest () { testMoneyEquals(); }
}The use of subclassing requires us to implement a subclass for each test case. This puts an additional burden on the tester. This is against the JUnit goal that the framework should make it as simple as possible to add a test case. In addition, creating a subclass for each testing method results in class bloat. Many classes with only a single method are not worth their costs and it will be difficult to come up with meaningful names.

Java provides anonymous inner classes which provide an interesting Java-specific solution to the class naming problem. With anonymous inner classes we can create an Adapter without having to invent a class name:

TestCase test= new MoneyTest(“testMoneyEquals “) {
    protected void runTest() { testMoneyEquals(); }
};This is much more convenient than full subclassing. It preserves compile-time type checking at the cost of some burden on the developer. Smalltalk Best Practice Patterns describes another solution for the problem of different instances behaving differently under the common heading of pluggable behavior. The idea is to use a single class which can be parameterized to perform different logic without requiring subclassing.

The simplest form of pluggable behavior is the Pluggable Selector. Pluggable Selector stores a Smalltalk method selector in an instance variable. This idea is not limited to Smalltalk. It is also applicable to Java. In Java there is no notion of a method selector. However, the Java reflection API allows us to invoke a method from a string representing the method’s name. We can use this feature to implement a pluggable selector in Java. As an aside, we usually don’t use reflection in ordinary application code. In our case we are dealing with an infrastructure framework and it is therefore OK to wear the reflection hat.

JUnit offers the client the choice of using pluggable selector or implementing an anonymous adapter class as shown above. To do so, we provide the pluggable selector as the default implementation of the runTest method. In this case the name of the test case has to correspond to the name of a test method. We use reflection to invoke the method as shown below. First we look up the Method object. Once we have the method object we can invoke it and pass its arguments. Since our test methods take no arguments we can pass an empty argument array:

protected void runTest() throws Throwable {
    Method runMethod= null;
    try {
        runMethod= getClass().getMethod(fName, new Class[0]);
    } catch (NoSuchMethodException e) {
        assertTrue(“Method \””+fName+”\” not found”, false);
    }
    try {
        runMethod.invoke(this, new Class[0]);
    }
    // catch InvocationTargetException and IllegalAccessException
}The JDK 1.1 reflection API only allows us to find public methods. For this reason you have to declare the test methods as public, otherwise you will get a NoSuchMethodException.

Here is the next design snapshot, with Adapter and Pluggable Selector added.

Figure 4: TestCase applies either Adapter with an anonymous inner class or Pluggable Selector

3.5 Don’t care about one or many – TestSuite

To get confidence in the state of a system we need to run many tests. Up to this point JUnit can run a single test case and report the result in a TestResult. Our next challenge is to extend it so that it can run many different tests. This problem can be solved easily when the invoker of the tests doesn’t have to care about whether it runs one or many test cases. A popular pattern to pull out in such a situation is Composite. To quote its intent “Compose objects into tree structures to represent part-whole hierarchies. Composite lets clients treat individual objects and compositions of objects uniformly.” The point about part-whole hierarchies is of interest here. We want to support suites of suites of suites of tests.

Composite introduces the following participants:

  • Component: declares the interface we want to use to interact with our tests.
  • Composite: implements this interface and maintains a collection of tests.
  • Leaf: represents a test case in a composition that conforms to the Component interface.

The pattern tells us to introduce an abstract class which defines the common interface for single and composite objects. The primary purpose of the class is to define an interface. When applying Composite in Java we prefer to define an interface and not an abstract class. Using an interface avoids committing JUnit to a specific base class for tests. All that is required is that the tests conform to this interface. We therefore tweak the pattern description and introduce a Test interface:

public interface Test {
    public abstract void run(TestResult result);
}TestCase corresponds to a Leaf in Composite and implements this interface as we have seen above.

Next, we introduce the Composite participant. We name the class TestSuite. A TestSuite keeps its child tests in a Vector:

public class TestSuite implements Test {
    private Vector fTests= new Vector();
}The run() method delegates to its children:

public void run(TestResult result) {
    for (Enumeration e= fTests.elements(); e.hasMoreElements(); ) {
        Test test= (Test)e.nextElement();
        test.run(result);
    }
}Figure 5: TestSuite applies Composite

Finally, clients have to be able to add tests to a suite, they can do so with the method addTest:

public void addTest(Test test) {
    fTests.addElement(test);
}Notice how all of the above code only depends on the Test interface. Since both TestCase and TestSuite conform to the Test interface we can recursively compose suites of test suites. All developers can create their own TestSuites. We can run them all by creating a TestSuite composed of those suites.

Here is an example of creating a TestSuite:

public static Test suite() {
    TestSuite suite= new TestSuite();
    suite.addTest(new MoneyTest(“testMoneyEquals”));
    suite.addTest(new MoneyTest(“testSimpleAdd”));
}This works fine, but it requires us to add all the tests to a suite manually. Early adopters of JUnit told us this was stupid. Whenever you write a new test case you have to remember to add it to a static suite() method, otherwise it will not be run. We added a convenience constructor to TestSuite which takes the test case class as an argument. Its purpose is to extract the test methods and create a suite containing them. The test methods must follow the simple convention that they start with the prefix “test” and take no arguments. The convenience constructor uses this convention, constructing the test objects by using reflection to find the testing methods. Using this constructor the above code is simplified to:

public static Test suite() {
    return new TestSuite(MoneyTest.class);
}The original way is still useful when you want to run a subset of the test cases only.

3.6 Summary

We are at the end of our cook’s tour through JUnit. The following figure shows the design of JUnit at a glance explained with patterns.

Figure 6: JUnit Patterns Summary

Notice how TestCase, the central abstraction in the framework, is involved in four patterns. Pictures of mature object designs show this same “pattern density”. The star of the design has a rich set of relationships with the supporting players.

Here is another way of looking at all of the patterns in JUnit. In this storyboard you see an abstract representation of the effect of each of the patterns in turn. So, the Command pattern creates the TestCase class, the Template Method pattern creates the run method, and so on. (The notation of the storyboard is the notation of figure 6 with all the text deleted).

Figure 7: JUnit Pattern Storyboard

One point to notice about the storyboard is how the complexity of the picture jumps when we apply Composite. This is pictorial corroboration for our intuition that Composite is a powerful pattern, but that it “complicates the picture.” It should therefore be used with caution.

4. Conclusion

To conclude, let’s make some general observations:

    • Patterns

We found discussing the design in terms of patterns to be invaluable, both as we were developing the framework and as we try to explain it to others. You are now in a perfect position to judge whether describing a framework with patterns is effective. If you liked the discussion above, try the same style of presentation for your own system.

    • Pattern density

There is a high pattern “density” around TestCase, which is the key abstraction of JUnit. Designs with high pattern density are easier to use but harder to change. We have found that such a high pattern density around key abstractions is common for mature frameworks. The opposite should be true of immature frameworks – they should have low pattern density. Once you discover what problem you are really solving, then you can begin to “compress” the solution, leading to a denser and denser field of patterns where they provide leverage.

    • Eat your own dog food

As soon as we had the base unit testing functionality implemented, we applied it ourselves. A TestTest verifies that the framework reports the correct results for errors, successes, and failures. We found this invaluable as we continued to evolve the design of the framework. We found that the most challenging application of JUnit was testing its own behavior.

    • Intersection, not union

There is a temptation in framework development to include every feature you can. After all, you want to make the framework as valuable as possible. However, there is a counteracting force- developers have to decide to use your framework. The fewer features the framework has, the easier it is to learn, the more likely a developer will use it. JUnit is written in this style. It implements only those features absolutely essential to running tests- running suites of tests, isolating the execution of tests from each other, and running tests automatically. Sure, we couldn’t resist adding some features but we were careful to put them into their own extensions package (test.extensions). A notable member of this package is a TestDecorator allowing execution of additional code before and after a test.

    • Framework writers read their code

We spent far more time reading the JUnit code than we spent writing it, and nearly as much time removing duplicate functionality as we spent adding new functionality. We experimented aggressively with the design, adding new classes and moving responsibility around in as many different ways as we could imagine. We were rewarded (and are still being rewarded) for our monomania by a continuous flow of insights into JUnit, testing, object design, framework development, and opportunities for further articles.

The latest version of JUnit can be downloaded from http://www.junit.org.

5. Acknowledgements

Thanks to John Vlissides, Ralph Johnson, and Nick Edgar for careful reading and gentle correction.

숫자야구게임 TDD 연습

최범균님이 TDD로 숫자야구게임을 만든 영상을 보고 spock으로 따라 만들어 보았습니다.

최범균님이 만든 자바코드와 비교해서 보시면 재밌으실 것 같습니다.

추가로, 랜덤하게 3자리 숫자를 생성하는 생성기가 몇번만에 게임넘버를 맞추는지

테스트하는 RandomGameNumberTest.groovy를 추가했습니다.

xUnit 실습

켄트벡의 “테스트 주도 개발(Test-Driven Development)“책에 있는 xUnit예제를

groovy로 만들어 보았습니다.

Github : https://github.com/bluepoet/xUnit-exercise

이왕이면, 자신의 주력언어 이외 현재 관심이 있거나 공부해보고 싶은 프로그래밍 언어로

만들어보면 더욱 재미있을 것 같습니다.

실행은 TestCaseTest.groovy를 참고하시면 됩니다.

package net.bluepoet.xunit

/**
 * Created by bluepoet on 2016. 11. 25..
 */
class TestCaseTest {
    static void main(String[] args) {
        testTemplateMethod(setUp())
        testResult(setUp())
        testFailedResult(setUp())
        testFailedResultFormatting(setUp())
        testSuite(setUp())
    }

    static def setUp() {
        return new TestResult()
    }

    static def testTemplateMethod(result) {
        def test = new WasRun("testMethod")
        test.run(result)
        assert test.log == 'setUp testMethod tearDown'
    }

    static def testResult(result) {
        def test = new WasRun("testMethod")
        test.run(result)
        assert result.summary() == '1 run, 0 failed'
    }

    static def testFailedResult(result) {
        def test = new WasRun("testBrokenMethod")
        test.run(result)
        assert result.summary() == '1 run, 1 failed'
    }

    static def testFailedResultFormatting(result) {
        result.testStarted()
        result.testFailed()
        assert result.summary() == '1 run, 1 failed'
    }

    static def testSuite(result) {
        def suite = new TestSuite()
        suite.add(new WasRun("testMethod"))
        suite.add(new WasRun("testBrokenMethod"))
        suite.run(result)
        assert result.summary() == '2 run, 1 failed'
    }
}

[번역]Spock Primer

Spock의 공식문서 중 Spock Primer를 번역해 보았습니다.

보실 때 오역이 있음을 감안하시고, 번역하기에 애매한 부분은 원문 그대로 옮겼습니다.


이번 챕터는 당신이 Groovy와 unit testing에 기본적인 지식을 가지고 있음을 가정한다.

만약 당신이 자바 개발자인데 Groovy에 대해 듣지 못했더라도 걱정하지 말라.

Groovy는 당신에게 매우 친숙하게 느껴질 것이다.

사실 , Groovy의 주요 설계목표 중 하나는 자바와 함께 scripting language가 되는 것이다.

따라서 Groovy 문서를 참고하기만 하면 언제든지 따라 할 수 있다.

이번 챕터의 목표는 현실에서 Spock 명세를 사용하고 당신이 더 많은 것을 원한다면 Spock을

충분히 가르치는 데 있다.

Groovy에 대해 더 배우고 싶다면 http://groovy-lang.org/로 가라.

unit testing에 대해 더 배우고 싶다면 http://en.wikipedia.org/wiki/Unit_testing로 가라.

전문 용어(Terminology)

몇가지 정의들로 시작해보자. Spock은 system of interest에 의해 나타난 예상된 기능

(properties, aspects)을 설명하는 명세를 작성할 수 있다.

The system of interest는 단일 클래스와 전체 application간에 있을 수 있으며,

system under specification(SUS)라 불린다.

기능설명은 SUS와 해당 협력자들의 구체적인 snapshot으로부터 시작된다.

이 snapshot은 기능의 fixture로 불린다.

이어지는 섹션에서 당신은 Spock 명세가 구성된 모든 building blocks을 살펴볼 것이다.

일반적인 명세는 그 중 일부만 사용한다.

Imports

 import spock.lang.*
 

spock.lang Package는 명세를 작성하기 위한 가장 중요한 타입들을 포함한다.

Specification

 class MyFirstSpecification extends Specification {
 // fields
 // fixture methods
 // feature
 // methods
 // helper methods
 }
 

specification은 spock.lang.Specification을 상속하는 Groovy class를 나타낸다.

specification의 이름은 보통 시스템 혹은 specification에 의해 나타난 시스템 운영과 관련된다.

예를 들어, CustomSpec, H264VideoPlayBack 그리고 AspaceshipAttackedFromTwoSides

모두 specification을 위한 합리적인 이름이다.

Class Specification은 명세작성을 위한 다수의 유용한 메서드들을 포함한다.

게다가 그것은 Spock의 Junit runner인 Sputnik와 함께 명세를 실행하라고 Junit에게

지시한다. Sputnik에게 감사하고, Spock specifications는 대부분 요즘 자바 IDEs와 빌드툴에

의해 실행될 수 있다.

Fields

 def obj = new ClassUnderSpecification()
 def coll = new Collaborator()
 

instance 필드들은 명세의 fixture에 속한 객체들을 저장하기 좋은 위치이다.

그것은 선언지점에서 바로 초기화 하는 것이 좋다

(의미적으론, 이것은 setup() method의 가장 처음에 그들을 초기화 하는 것과 같다)

instance 필드에 저장된 객체는 feature method 사이에 공유되지 않는다.

대신, 모든 feature method는 자신만의 객체를 얻는다.

이것은 각각의 feature method들을 분리하는데 도움을 주고, 그것은 가장 바람직한 목표이다.

 @Shared res = new VeryExpensiveResource()
 

때때로 feature method들 사이에 객체를 공유하는 것이 필요하다.

예를 들어, 객체 생성비용이 크거나 혹은 feature method들 각각이 상호작용을 해야 될 때가 있다.

이것을 하기 위해, @Shared 필드를 선언한다.

그것은 선언하는 시점에서 필드를 초기화하는 것이 가장 좋다.

(의미적으로 이것은 setupSpec() method의 가장 처음에 필드를 초기화 하는 것과 같다)

 static final PI = 3.141592654
 

static fields는 상수를 위해 사용되어진다.

그렇지 않으면 공유된 필드들을 더 선호한다. 왜냐하면 공유하는 측면에서 그들의 의미가

더 잘 정의되었기 때문이다.

Fixture Methods

 def setup() {} // run before every feature method
 def cleanup() {} // run after every feature method
 def setupSpec() {} // run before the first feature method
 def cleanupSpec() {} // run after the last feature method
 

Fixture methods는 feature method가 실행되는 설정과 환경을 정리하는 역할을 맡는다.

보통 모든 feature method를 위해 setup() 그리고 cleanup() method 등에 해당하는

신선한 fixture를 사용하는 것은 좋은 생각이다.

모든 fixture methods는 선택이다.

때로는 feature method가 fixture를 공유하는 것이 의미가 있으며, 이는 setupSpec() 및

cleanupSpec() 메소드와 함께 공유 필드를 사용하여 수행된다.

setupSpec()이나 cleanupSpec()은 그들이 @Shared와 함께 어노테이션되지 않았다면

instance fields를 참조할 수 없다.

fixture methods가 specification subclass안에서 대체되면 superclass의 setup()은

subclass의 setup()전에 실행될 것이다. cleanup()은 역순으로 실행된다.

subclass의 cleanup()이 superclass cleanup()보다 먼저 실행될 것이다.

setupSpec()이나 cleanupSpec()은 같은 방식으로 실행한다.

spock은 상속계층의 모든 레벨에서 fixture methods들을 자동으로 찾아 실행하므로 명시적으로

super.setup()이나 super.cleanup()을 호출할 필요가 없다.

Feature Methods

 def "pushing an element on the stack"() {
 // blocks go here
 }
 

Feature Methods는 명세의 중심이다.

그것은 명세에 따라 시스템에서 찾을 것으로 예상되는 features(속성, 측면)을 설명한다.

관례상, feature methods는 String literals과 함께 명명되어진다.

당신의 feature methods를 위해 좋은 이름을 선택하도록 노력하라.

당신이 좋아하는 어떤 문자든 자유롭게 사용할 수 있다.

개념상, 하나의 feature method는 4개의 단계로 구성되어 있다.

1. feature’s fixture의 설정
2. 명세에 따라 시스템에 실행 제공
3. 시스템으로부터 예상된 반응 설명
4. feature’s fixture 정리

첫번째와 마지막 단계가 선택적임에도, 실행과 반응단계는 항상 존재한다.

(상호작용하는 feature methods 제외), 그것은 여러번 일어날 수 있다.

Blocks

Spock은 feature method의 각각의 개념적인 단계를 구현하기 위한 지원이 내장되어 있다.

이를 위해 feature methods는 소위 blocks라 불리는 것으로 구성된다.

Blocks는 라벨과 함께 시작하고, 다음 블락의 시작이나 method의 끝으로 확장된다.

blocks는 6가지 종류가 있다. setup, when, then, expect, cleanup, where blocks.

method 시작과 첫번째 명시적인 블록 사이에 어느 구문들은 암시적으로 setup block에 속한다.

feature method에는 적어도 하나의 명시적(레이블이 지정된)인 블록이 있어야한다.

실제로 명시적 블록이 있으면 method를 feature method로 만드는 것이다.

블록은 method를 별개의 부분으로 나누고 중첩 될 수 없습니다.

우측(편의상 아래)의 그림은 blocks이 feature method의 개념적인 단계와 어떻게 맵핑되는지

보여준다.

%e1%84%89%e1%85%b3%e1%84%8f%e1%85%b3%e1%84%85%e1%85%b5%e1%86%ab%e1%84%89%e1%85%a3%e1%86%ba-2016-11-23-%e1%84%8b%e1%85%a9%e1%84%92%e1%85%ae-11-18-30

where block은 특별한 역할을 가지고 있고, 곧 보여질 것이다.

그러나 첫번째로, 다른 blocks를 더 자세히 살펴보자.

Setup Blocks

 setup:
 def stack = new Stack()
 def elem = "push me"
 

setup block은 설명하는 기능에 대한 설정작업을 수행하는 곳이다.

그것은 다른 블록에 의해 선행될 수도 없고, 반복될 수도 없다.

setup block은 어떤 특별한 의미를 가지지 않는다.

setup: label은 선택적이고 생략될 수 있으므로, 결과적으로 암시적인 setup block이 된다.

given: label은 setup:의 별칭이고 때때로 더 가독성 있는 feature method 설명으로 이어진다

(문서는 Specificatons을 참고하라)

When and Then Blocks

 when: // stimulus
 then: // response
 

when과 then blocks는 항상 같이 나타난다. 그들은 실행하고 예상된 결과를 설명한다.

when blocks이 임의의 코드를 포함함에도, then blocks은 상태,예외 상태, 상호작용,

그리고 변수정의로 제한된다. feature method는 여러쌍의 when-then blocks를 포함할 수 있다.

Conditions

Conditions은 Junit의 단정문과 매우 흡사한 예상되는 상태를 설명한다.

그럼에도 불구하고 Conditions는 일반적인 boolean expressions으로 작성되므로,

단정문 API가 필요없다. (더 정확하게는, condition은 또한 boolean value가 아닌 것을

만들 수 있으며, 그 다음 Groovy truth에 따라 평가된다)

실제 몇가지 conditions를 살펴보자

 when:
 stack.push(elem)

then:
 !stack.empty
 stack.size() == 1
 stack.peek() == elem
 

TIP : feature method당 conditions의 수를 가급적 작게 유지하라.

하나에서 5개 정도가 좋은 가이드라인이다. 만약 당신이 저것보다 더 많이 가지고 있다면,

한번에 여러 관련없는 기능들을 넣었는지 스스로에게 자문해보라.

대답이 맞다면, 몇몇 작은 것들로 feature method를 분리하라.

당신의 conditions가 오직 그들의 값들에 대해서만 다르다면, data table 사용을 고려해보라

condition이 잘못되었을 때 Spock이 제공하는 피드백은 어떤 종류가 있을까?

두번째 condition을 2로 바꿔보자. 여기 우리가 얻을 수 있는 것이 있다.

 Condition not satisfied:

stack.size() == 2
 | | |
 | 1 false
 [push me]
 

당신이 보는것처럼, Spock은 condition을 평가하는 동안 만들어진 모든 값들을 캡쳐하고

그들을 쉽게 이해할 수 있는 형태로 보여준다. 좋지 않은가?

Implicit and explicit conditions

Conditions는 then과 expect blocks에서는 필수적인 재료이다.

상호작용으로 분류된 method와 표현식은 void method를 호출하는 경우를 제외하고,

이 블록안에서 모든 최상위 표현식들은 conditions로 암시적으로 처리된다.

다른 위치에서 conditions를 사용하기 위해선, 당신은 Groovy의 assert keyword와

함께 그들을 지정하는 것이 필요하다.

 def setup() {
 stack = new Stack()
 assert stack.empty
 }
 

만약 명시적인 조건이 잘못됐다면, 그것은 암시적인 조건과 동일한 멋진 진단메세지를

만들어 낼 것이다.

Exception Conditions

예외 조건은 when block에서 예외를 던지는 것을 설명할 때 사용된다.

그들은 thrown() method 사용으로 정의되며, 예상된 예외타입을 전달한다.

예를 들어, 빈 스택에서 가져오는 것을 설명할 때 EmptyStackException을 던질 수 있다.

당신은 아래와 같이 쓸 수 있다.

 when:
 stack.pop()

then:
 thrown(EmptyStackException)
 stack.empty
 

당신이 보는 것처럼, 예외조건은 다른 조건들(및 다른 블록)이 이어질 수 있다.

이것은 특별히 예외의 예상된 내용을 명시하는데 유용하다.

예외에 접근하기 위해, 첫번재로 변수에 그것을 결합해라

 when:
 stack.pop()

then:
 def e = thrown(EmptyStackException)
 e.cause == null
 

이 문법은 두 가지 이점이 있다.

첫번째로, 예외변수는 강력하게 형식화되어, IDE가 코드완성을 제공하기가 더 수월해진다.

두번째로, 조건은 조금은 문장과 비슷하게 읽힌다.(그리고 EmptyStackException이 던져진다)

예외타입이 thrown() method에 전달되지 않으면, 왼쪽의 변수 타입으로 유추한다.

때때로 우리는 예외가 던져질 수 없음을 전달할 필요가 있다.

예를 들어, HashMap에 null key를 넣는다고 해보자.

 def "HashMap accepts null key"() {
 setup:
 def map = new HashMap()
 map.put(null, "elem")
 }
 

이것은 작동하지만 코드의 의도가 드러나지 않는다. 이 메서드 구현을 끝내기 전에

개발을 끝낸 누군가가 있는가? 결국, 조건은 어디에 있는가?

다행히도 우리는 더 잘 할 수 있다.

 def "HashMap accepts null key"() {
 setup:
 def map = new HashMap()

when:
 map.put(null, "elem")

then:
 notThrown(NullPointerException)
 }
 

notThrown()을 사용함으로써, 우리는 NullPointerException이 던져지지 않는다는 것을

분명히 할 수 있다.

(Map.put()의 명세에 따라, 이것은 null keys를 지원하지 않는 맵을 위해선 옳은 것이

될 것이다) 그럼에도 불구하고, 이 메서드는 어떤 다른 예외가 던져진다면 실패할 것이다.

Interactions

조건들은 객체의 상태를 설명함에 비해,  상호작용은 객체들이 서로 어떻게 협력하는지 보여준다.

상호작용은 전체 챕터에서 다루므로 우리는 여기서 간단한 예제를 보여주겠다.

우리는 publisher으로부터 그것의 subscribers에게 가는 이벤트들의 흐름을 설명한다고

가정해보자. 여기 코드가 있다.

def "events are published to all subscribers"() {
  def subscriber1 = Mock(Subscriber)
  def subscriber2 = Mock(Subscriber)
  def publisher = new Publisher()
  publisher.add(subscriber1)
  publisher.add(subscriber2)

  when:
  publisher.fire("event")

  then:
  1 * subscriber1.receive("event")
  1 * subscriber2.receive("event")
}

Expect Blocks

expect block은 조건과 변수 정의만 할 수 있다는 점에서 then block보다 더 제한적이다.

단일 표현에서 실행과 예상되는 반응을 설명하기 더 자연스러운 상황에서 유용하다.

예를 들어 Math.max() method를 설명하는 두가지 시도를 비교해보자

when:
def x = Math.max(1, 2)

then:
x == 2
expect:
Math.max(1, 2) == 2

두 코드는 의미적으로 비슷함에도 불구하고, 두번째가 분명히 더 바람직하다.

가이드에 따르면, side effects가 있는 methods를 설명할 때는 when-then block을

순전히 기능 methods를 설명할때는 expect를 사용해라.

TIP : any()나 every()같은 Groovy JDK 메서드들을 사용하여 보다 표현적이고 간결한 조건을

만들어라.

Cleanup Blocks

setup:
def file = new File("/some/path")
file.createNewFile()

// ...

cleanup:
file.delete()

cleanup block은 where block 다음에 오고, 반복되지 않는다.

cleanup 메서드처럼, feature method에 의해 사용된 어떤 자원들을 해제하고,

feature 메서드에서 예외가 발생되도 실행한다.

그 결과로, cleanup block은 최악의 경우에 feature 메서드의 첫번째 실행문에서 예외를 던져도

모든 지역변수들이 그들의 기본 값들을 갖도록 정상적으로 처리되어야한다.

TIP : Groovy의 안전한 방어 연산자(foo?.bar())는 방어코드 작성을 단순화한다

객체레벨 명세에서는 보통 cleanup 메서드가 필요하지 않다.

그들이 소비하는 자원은 garbage collector에 의해 자동으로 반환되는 메모리이다.

좀 더 조잡한 명세에서는 그럼에도 불구하고, 파일시스템을 정리할 때 cleanup block을

사용하거나 database connection을 닫을 때, 혹은 network service를 셧다운 시킬때

사용한다.

TIP : 모든 feature 메서드들이 같은 자원을 필요로 하도록 설계되었을 경우,

cleanup() method를 사용하라. 그렇지 않으면 cleanup block을 선호해라.

이와 같은 절충은 setup() methods와 setup blocks에도 적용된다.

Where Blocks

where block은 항상 메서드의 마지막에 위치하고, 반복되지 않는다.

그것은 데이터 기반 feature method에 사용된다..

이것이 어떻게 동작하는지 알려주기 위해, 이어지는 예제를 보자.

def "computing the maximum of two numbers"() {
  expect:
  Math.max(a, b) == c

  where:
  a << [5, 3]
  b << [1, 9]
  c << [5, 9]
}

where block은 feature method의 두가지 버전을 효율적으로 만든다.

하나는 a가 5, b가 1, c가 5이고 다른 하나는 a가 3, b가 9, c가 9이다.

마지막으로 선언되었지만 feature method가 실행되기 전에 where 블록이 평가된다.

where 블록은 Data Driven Testing 장에서 자세히 설명한다.

Helper Methods

때로는 feature method가 커지거나 중복된 코드가 많이 포함될 수 있다.

그러한 경우 하나 이상의 helper method를 도입하는 것이 타당하다.

helper method의 두 가지 좋은 후보는 setup/cleanup logic과 복잡한 조건들이다

전자를 분석하는 것은 간단하므로 조건들을 살펴보겠다.

def "offered PC matches preferred configuration"() {
  when:
  def pc = shop.buyPc()

  then:
  pc.vendor == "Sunny"
  pc.clockRate >= 2333
  pc.ram >= 4096
  pc.os == "Linux"
}

당신이 컴퓨터를 매우 좋아한다면(geek), 당신이 선호하는 PC 구성이 매우 상세해서,

많은 다른 상점의 제안을 비교하길 원할 것이다. 그러므로 조건을 분석해보자

def "offered PC matches preferred configuration"() {
  when:
  def pc = shop.buyPc()

  then:
  matchesPreferredConfiguration(pc)
}

def matchesPreferredConfiguration(pc) {
  pc.vendor == "Sunny"
  && pc.clockRate >= 2333
  && pc.ram >= 4096
  && pc.os == "Linux"
}

새로운 helper method matchesPreferredConfiguration()는 결과를 반환하는

단일 boolean 표현식으로 구성된다.(return keyword는 Groovy에서 선택이다)

부적절한 제안이 제시되는 것을 제외하면 이것은 괜찮다.

Condition not satisfied:

matchesPreferredConfiguration(pc)
|                             |
false                         ...

이것은 별로 도움이 되지 않는다. 하지만 운좋게도 우리는 더 잘할 수 있다.

void matchesPreferredConfiguration(pc) {
  assert pc.vendor == "Sunny"
  assert pc.clockRate >= 2333
  assert pc.ram >= 4096
  assert pc.os == "Linux"
}

helper method로 조건을 분해 할 때는 두 가지 점을 고려해야한다.

첫째, 암시적 조건은 assert 키워드를 사용하여 명시적 조건으로 변환해야한다.

둘째, helper method에는 반환 형식 void가 있어야 한다.

그렇지 않으면 Spock은 반환 값을 실패한 조건으로 해석 할 수 있다.

이는 우리가 원하는 것이 아니다.

예상한대로, 개선된 helper method는 우리에게 잘못되었다고 말해준다.

Condition not satisfied:

assert pc.clockRate >= 2333
       |  |         |
       |  1666      false
       ...

마지막 조언 : 코드 재사용은 일반적으로 좋은 것이지만, 너무 과하게 사용하지마라.

fixture와 helper methods의 사용이 feature methods간의 결합도가 증가된다는 것을

알아야 한다.

만약 재사용을 너무 많이 하거나 잘못한다면, 당신은 깨지기 쉽고 개선하기 어려운 명세가

될 것이다.

Using with for expectations

위의 helper method 대신에, 당신은 with(target, closure) method로 검증해야 될 객체와

상호작용할 때 사용할 수 있다. 이것은 특별히 then과 expect block에 유용하다

def "offered PC matches preferred configuration"() {
  when:
  def pc = shop.buyPc()

  then:
  with(pc) {
    vendor == "Sunny"
    clockRate >= 2333
    ram >= 406
    os == "Linux"
  }
}

당신이 helper method를 좋아하지 않는다면, 적절한 에러 리포팅을 위한 명시적인 선언문이

필요없다. mocks를 검증할 때, with 구문은 자세한 검증구문을 생략할 수 있다.

def service = Mock(Service) // has start(), stop(), and doWork() methods
def app = new Application(service) // controls the lifecycle of the service

when:
app.run()

then:
with(service) {
  1 * start()
  1 * doWork()
  1 * stop()
}

Specifications as Documentation

잘 쓰여진 명세는 귀중한 정보원이다. 특히 개발자 (건축가, 도메인 전문가, 고객등)보다

광범위한 고객을 대상으로하는 고급명세의 경우 명세 및 기능의 이름뿐 아니라 자연스러운

언어로 더 많은 정보를 제공하는 것이 좋다.

따라서 Spock은 블록에 텍스트 설명을 첨부하는 방법을 제공한다.

setup: "open a database connection"
// code goes here

블록의 개별부분은 and:로 설명할 수 있다.

setup: "open a database connection"
// code goes here

and: "seed the customer table"
// code goes here

and: "seed the product table"
// code goes here

and : label 뒤에 description을 붙이는 것은 메소드의 의미를 변경하지 않고 feature 메소드의

어떤(최상위)위치라도 삽입 할 수 있다.

Behavior Driven Development (행위 주도 개발)에서는 고객 지향적인 기능은(스토리라고 함)

given-when-then 형태로 설명된다.

Spock은 given: 라벨과 같은 명세의 스타일을 직접 지원한다.

given: "an empty bank account"
// ...

when: "the account is credited $10"
// ...

then: "the account's balance is $10"
// ...

앞에서 언급했듯이 주어진 given:은 setup의 별칭이다.

블록 설명은 소스 코드에 존재할뿐만 아니라 Spock 런타임에서도 사용할 수 있다.

블록 설명의 계획된 사용은 강화된 진단 메시지 및 모든 이해 관계자가 동일하게 이해하는

텍스트 보고서이다.

Extensions

지금까지 보았 듯이 Spock은 명세 작성을 위한 많은 기능을 제공한다.

그러나 다른 어떤 것이 필요할 때가 항상 있습니다.

따라서 Spock은 차단 기반 확장 메커니즘을 제공합니다.

확장 기능은 지시문이라 불리는 어노테이션에 의해 활성화됩니다.

현재 Spock에는 다음과 같은 지침이 포함되어 있습니다.

@Timeout
feature 및 fixture method 실행을 위한 타임아웃 설정

@Ignore
feature method실행 무시

@IgnoreRest
이 어노테이션을 가지고 있지 않은 모든 feature method는 무시된다.

빠르게 단일 메서드를 실행하고 싶을 때 유용하다

@FailsWith
feature method가 갑자기 완료 될 것으로 기대한다. @FailsWith에는 두 가지 사용 사례가 있다.

첫째, 즉시 해결할 수없는 알려진 버그를 문서화하는 것이다.

둘째, 특정 상황에서 예외 조건을 바꿀 수 있다 (예외 조건의 동작 지정과 같이).

다른 모든 경우에는 예외 조건이 바람직하다.

자신의 지시문과 확장을 구현하는 방법을 배우려면 Extensions 장으로 이동하라.

Comparison to JUnit

Spock은 다른 용어를 사용하지만 JUnit에서 영감을 얻은 많은 개념과 기능이 있다.

다음은 대략적인 비교이다.

%e1%84%89%e1%85%b3%e1%84%8f%e1%85%b3%e1%84%85%e1%85%b5%e1%86%ab%e1%84%89%e1%85%a3%e1%86%ba-2016-11-24-%e1%84%8b%e1%85%a9%e1%84%8c%e1%85%a5%e1%86%ab-12-31-11

[Spock]spock-dbunit 초간단 예제

기존 스프링 프레임워크 기반에서 어플리케이션 개발시, DB와 연관된 테스트코드를 만들기 위해

spring-test-dbunit 라이브러리를 사용했습니다.

최근 프로젝트에서 스프링부트를 사용하면서, 다시 DB에 대해 테스트코드를 만들어야 되는 일이

생겼는데요. 테스트 프레임워크는  전부터 즐겨쓰던 groovy기반의 spock을 사용하고 있습니다.

spock 역시 dbunit관련 라이브러리가 있을까 살펴보던 중, spock-dbunit을 발견해서

간단히 CRUD 예제를 만들어 보았습니다.

전에 spring-test-dbunit을 사용할 때는, 별도의 xml파일을 만들어야 되서 상당히 귀찮고

오타도 많아 테스트코드 작성시 어려움이 많았었지만, spock-dbunit의 모토가

그 어려움과 귀찮음을 피하는 것이라고 하니 개인적인 입장에서는 상당히 호감이 갔습니다.

그리고 무엇보다 sql query를 소스상에 직접 작성해서 테스트코드를 만들 수 있는 점도

큰 장점으로 생각되네요.

물론, 개인개발자가 1인이 만들었고 처음 만든게 2013년 중반임에도 아직 버전이 0.2라서

spring-test-dbunit보다 상대적으로 기능상 쓰기에 더 불편 할수도 있습니다.

package net.bluepoet.exercise

import be.janbols.spock.extension.dbunit.DbUnit
import groovy.sql.Sql
import org.apache.tomcat.jdbc.pool.DataSource
import spock.lang.Specification

/**
 * Created by bluepoet on 2016. 11. 17..
 */
class SpockDBUnitTest extends Specification {

    DataSource dataSource

    @DbUnit
    def content = {
        User(id: 1, name: 'bluepoet', age: 20, 'phone_number': '010-0000-0001')
        User(id: 2, name: 'tester', age: 10, 'phone_number': '010-0000-0002')
    }

    def setup() {
        dataSource = new DataSource()
        dataSource.driverClassName = 'org.h2.Driver'
        dataSource.url = 'jdbc:h2:mem:'
        dataSource.username = 'sa'
        dataSource.password = ''
        new Sql(dataSource).execute("CREATE TABLE User(id INT PRIMARY KEY, name VARCHAR(255), age INT, phone_number VARCHAR(20))")
    }

    def cleanup() {
        new Sql(dataSource).execute("drop table User")
    }

    def "총 유저수가 몇명인지 테스트한다."() {
        when:
        def result = new Sql(dataSource).firstRow("select count(*) as cnt from User")

        then:
        result.cnt == 2
    }

    def "특정 유저정보를 가져와 확인한다."() {
        expect:
        new Sql(dataSource).firstRow("select * from User where id = " + id) == result

        where:
        id | result
        1  | [ID: 1, NAME: 'bluepoet', AGE: 20, PHONE_NUMBER: '010-0000-0001']
        2  | [ID: 2, NAME: 'tester', AGE: 10, PHONE_NUMBER: '010-0000-0002']
    }

    def "특정 유저정보를 수정하고 결과를 확인한다."() {
        when:
        new Sql(dataSource).executeUpdate("update User set age=12, phone_number='010-1111-2222' where id=1")
        def result = new Sql(dataSource).firstRow("select * from User where id=1")

        then:
        result.age == 12
        result.phone_number == '010-1111-2222'
    }

    def "특정 유저를 삭제하고 총 유저수를 확인한다."() {
        when:
        new Sql(dataSource).executeUpdate("delete from User where id=2")
        def result = new Sql(dataSource).firstRow("select count(*) as cnt from User")

        then:
        result.cnt == 1
    }
}

Github url은 https://github.com/bluepoet/spock-dbunit-exercise 를 참고하세요.

[Spock] 포팅 경험기1

페이스북에 “테스트코드와 친해지기“라는 그룹을 만들고 처음으로 한 프로젝트가

최범균님의 DDD START책에 java + junit으로 되어 있는 테스트코드를 groovy + spock으로

포팅하는 것이었습니다.

해당 프로젝트는 ddd-repeat-start 에서 확인하실 수 있습니다.

테스트코드와 친하지 않더라도 java 개발자라면 junit으로 간단한 테스트코드는 만들어 본

경험이 있을텐데요.

굳이 기존에 익숙한 junit을 쓰지 않고 spock으로 포팅하게 된 이유를 51개의 테스트케이스를

포팅하면서 정리해 보았습니다.

* groovy언어만의 장점은 해당 내용에서 제외하였습니다.


1. 의존 라이브러리 추가

spock을 사용하기 위해 기존 최범균님 프로젝트 pom.xml 파일에서 아래 라이브러리를

추가했습니다.

기존 테스트코드가 spring boot로 되어 있었기 때문에 spock-spring 모듈을 사용했습니다.

<dependency>
<groupId>org.spockframework</groupId>
<artifactId>spock-spring</artifactId>
<version>1.0-groovy-2.4</version>
</dependency>

<dependency>
<groupId>cglib</groupId>
<artifactId>cglib-nodep</artifactId>
<version>3.2.4</version>
</dependency>

cglib는 class mocking을 위해 추가했습니다. (SecurityCancelPolicyTest.groovy에 사용)

2. given-when-then 템플릿을 프레임워크 차원에서 제공

spock의 장점은 BDD(Behavior Driven Development)에서 나온 템플릿인

given(테스트할 조건을 주고)-when(테스트를 실행한 뒤)-then(결과를 검증한다) 템플릿을

프레임워크에서 제공하고 있다는 점입니다.

예전에 junit을 사용할 때는 주석을 사용해 템플릿으로 만들어 쓰고 있었지만, 이제

프레임워크에서 제공해주다 보니 자연스럽게 이 패턴대로 테스트코드를 작성할 수 있습니다.


//junit
@Test
public void addTest() {
    // given
    int a = 1;
    int b = 2;
 
    // when
    int result = a + b;

    // then
    assertThat(result, is(3));
}


//spock
def "addTest"() {
    given:
    def a = 1
    def b = 2

    when:
    def result = a + b

    then:
    result == 3
}

둘 간의 차이점이 확 느껴지시나요? spock을 사용했을 때가 훨씬 명시적이고 직관적임을

알 수 있습니다.

3. 결과검증 간편

junit을 사용할 때는 기본적으로 Junit이 제공해주는 Assert 클래스의 기능과 부가적으로

hamcrest, fest assert, assertj등의 라이브러리를 사용해서 결과를 검증해야만 했습니다.

//junit 
assertThat(result, is(1));
assertThat(result, is(true));
assertThat(result, is("test"));
assertThat(result, is(1));
assertThat(result, instanceOf(TestClass.class));
assertThat(result, nullValue());
assertThat(result, notNullValue());

@Test(expected = IllegalArgumentException.class)

try {
    a.function();
}catch(IllegalArgumentException e) {
    fail();
}
   
//spock
then:
result == 1
result == true
result == "test"
result instanceof TestClass
result == null
result != null
thrown IllegalArgumentException
notThrown IllegalArgumentException

하지만 위의 예제에서도 나오듯이 spock에서는 상대적으로 결과검증이 간편하고 직관적입니다.

4. Mock 사용 편리

junit으로 유닛테스트를 만들 때, Mock사용을 위해 mockito 라이브러리를 많이 사용합니다.

하지만 spock을 사용하면 자체적으로 Mock기능을 사용할 수 있습니다.

//junit
@Mock
private Order order;

when(order.getOrderer()).thenReturn(new MemberID("user1"), "");
 
verify(order, times(2)).purchase();

//spock
//SecurityCancelPolicyTest.groovy
given:
def order = Mock(Order)
order.getOrderer() >> new Orderer(new MemberId("user1"), "")

then:
2 * order.purchase();

stub이나 spy등을 사용할 때도, mockito에 비해 상대적으로 명시적으로 사용할 수 있습니다.

그리고 무엇보다 결과검증을 할 때, 메소드 호출의 경우 “호출횟수 * 객체.메서드명()“같이

사용할 수 있어 편리합니다.

다만, PlaceOrderServiceTest.groovy에서는 ArgumentCaptor를 사용하기 위해

mockito를 사용했습니다.

then:
ArgumentCaptor<Order> orderCaptor = ArgumentCaptor.forClass(Order.class)
verify(mockOrderRepository).save(orderCaptor.capture())

Order createdOrder = orderCaptor.getValue()
createdOrder != null

5. 동일 실행구문에서 여러 조건으로 테스트 용이

테스트코드를 만들다 보면 실행구문은 동일한데 조건을 다르게 줘야 하는 경우가 있습니다.

보통 이럴 때는 조건에 따라 변수에 값 할당이나 객체를 다르게 만들어줘야 하는데요.

spock에서는 이런 상황에서 expect(혹은 when~then)~where로 보다 편리하게

테스트코드를 만들 수 있습니다.

def "EventStore에서 이벤트를 가져온다."() {
    given:
    List<EventEntry> entries = eventStore.get(1, 2)
    
    expect:
    SampleEvent event = objectMapper.readValue(entries.get(index).getPayload(), SampleEvent.class)
    event.getName() == name
    event.getValue() == value
    
    where:
    index | name    | value
    0     | "name2" | 12
    1     | "name3" | 13
}

6. 기타

그 밖에 spock을 사용하면서 아래와 같은 장점을 느꼈습니다.

  • 메서드명에 “한글“을 넣는 것이 junit에 비해 자연스럽다
  • private 필드나 메서드 접근이 가능하다
  • 테스트 실패시 나오는 결과를 보고 원인을 쉽게 알 수 있다.

다음 포스팅에서는 spock으로 포팅하면서 겪은 어려움이나 불편함에 대해 정리할 예정입니다.

* 위 내용 중 잘못된 내용이나 수정해야 할 부분이 있으면 언제든지 의견 부탁드립니다.

[독서]2016 올해 읽은 책

기술서적


  1. 스프링 입문을 위한 자바 객체 지향의 원리와 이해
  2. 객체지향과 디자인패턴(사내스터디)
  3. 가장 빨리 만나는 Docker(초반 7장까지 읽음)
  4. 아파치 Storm을 이용한 분산 실시간 빅데이터 처리(진행중)
  5. Ansible 설정관리(진행중)
  6. 아파치 Kafka 따라잡기(진행중)
  7. 주키퍼 Zookeeper(계획)
  8. HBase in Action
  9. Gradle 철저입문
  10. 헬로 데이터 과학

 

기술자료


 

 

 

교양서적


 

 

기타