JUnit Goodness

Embed Size (px)

Text of JUnit Goodness

  • Solnet Solutions LtdJUnit Goodness - October 2009

  • JUnit GoodnessParameterized TestsMatcher AssertionsThe @Ignore Annotation

    Coming at you from all angles in JUnit 4

  • Parameterized TestsWhy would you write a parameterized test?How do you write a parameterized test?

    Parameterized tests are generic tests that run multiple times using a collection of test parameters.

  • Why Would You Write A Parameterized TestsParameterized test are suitable for two types of test:Tests that require different data in specific environmentsTest that use multiple data sets (Data Driven testing)

    Through the use of parameterized tests we can also eliminate the following JUnit test smellsTest cases that contain a test method for each test data combinationTest cases that loop over a collection of values

    By implementing parameterized tests we are able to create tests in a DRY manner.

  • Eliminate JUnit Test Smell: Test cases that contain a test method for each test data combination

    @Testpublic void testNegativeSpendingMoneyBudget() { budget = new Budget("Negative Spending Budget", 1000.00, 560.00, 350.00, 100.00); System.out.println("Testing budget spending money: " + budget.getName()); assertEquals("Spending Money is not what was expected", -10.00, budget.getSpendingMoney(), 0.0);}

    ...similar test cases exist for testNoSpendingMoneyBudget and testPositiveSpendingMoneyBudget

    Issues with this JUnit test smell:

    Each new test instance requires the addition of a new test method

    Results in bloated test cases that reduce test suite maintainability

    Duplicated test code breaks the DRY principle

    In order to remove this smell we decide to refactor this test caseusing a loop

  • Eliminate JUnit Test Smell: Test cases that loop over a collection of values/** * Loop Test - The main problem with this kind of test is that if 1 test * fails the loop does not complete, so not all test instances are run. **/@Testpublic void testSpendingMoney() {

    for (Budget budget: budgets) { System.out.println("Testing budget spending money: " + budget.getName()); assertEquals("Spending Money is not what was expected", expectedSpendingMoney.get(budget.getName()).doubleValue(), budget.getSpendingMoney(), 0.0); }}Issues with this JUnit test smell:Single test failure will terminate the loopNot all test instance values will be run if a failure occursNo straightforward way to tell which test failed from failure message

  • More Reasons To Consider Parameterized TestsWhat happens when a new type of test is required when using either of the previous JUnit test smells?What happens if the class methods under test are renamed or refactored? How quickly can the test be refactored to be back up and running?What work would be involved to change the tests based on specific test environments?How simple is it to change the data set being used for testing for Data Driven testing?

  • How Do You Write A Parameterized TestCreate a generic test method decorated with the @Test annotationCreate a static feeder method that returns a Collection type and decorate it with the @Parameters annotationCreate class members for the parameter types required in generic test methodsCreate a constructor that takes the test parameter types and initializes the relevant class membersSpecify that the test case should be run using the Parameterized class via the @RunWith annotationFollow these 5 simple steps

  • Parameterized Tests ConsiderationsWhen using parameterized tests keep the following in mind:

    An instance of the enclosing class is created for each test run Keep non-parameterized tests out of the parameterized test classIdentification of any test instance failures is not straightforwardWhere are we getting the parameterized test data from

  • Named Parameterized Test ExtensionSimplify test failure identification through a custom named extension

    Extend the Suite test runner.Duplicate the private TestClassRunnerForParametersUpdate the getName() and testName() overriden methods with the identification details requiredEnsure that the Parameters annotation used is from the extended implementation

  • XML Data Parameterized Test ExtensionBased on JUext (JUnit Extensions) XMLParameterizedRunner

    JUnit extension exists, but does not work out of the boxGenerate a custom XMLParameterized implementationSimilar implementation to NamedParamertized test extensionUse a ParameterSet instead of a List[]getParametersList() method does the XML file look upJUnit Extension XML Parameter limited parameter digesterAble to create bean object instances similar to Spring (Budget)Added double to DigesterParameterFactory object rulesdata.xml file contains the test data

  • Matcher AssertionsMatcher assertions match object and values and composite matcher values. (Original implemented in JMock and integrated into JUnit)

    Core Matchers (Hamcrest)allOf(), any(), anyOf(), anything(), describedAs(), equalTo(), instanceOf(), is(), not(), notNullValue, nullValue(), sameInstance()

    JUnit Matcherboth(), containsString(), either(), everyItem(), hasItem(), hasItems() What are matcher assertions?

  • Matcher AssertionsReasons to use the matcher assertions?

    More readableImproved readability of failure messagesMatcher combinations/List matchersCustom matchersassertThat([value], [matcher statement]);

  • Matchers ReadableassertThat(budget.getName(), is(Budget Name))- subject, verb, object- Assert that the budgets name is Budget NameassertEquals(Budget Name, budget.getName())- verb, object, subject- Assert equals is Budget Name the budgets nameassertThat(budget.getIncome, is(1000.00)- Assert that the budgets income is 1000.00assertEquals(1000.00, budget.getIncome())- Assert equals is 1000.00 the budgets income

  • Matchers Readable Failure MessagesassertThat(budget.getName(),

    is(Not Negative Spending Budget))- Failure Messagejava.lang.AssertionErrorExpected: is Not Negative Spending Budget got: Negative Spending Budget

    assertEquals(Not Negative Spending Budget,

    budget.getName())- Failure Messageorg.junit.ComparisonFailure: expected: but was:

  • Matchers Combinations/Lists/CustomCombination Matcher Assertions:

    Combinations can be used to negate matchesassertThat(budget.getIncome(), is(not(equalTo(800.00))))Combinations can be used to combine matchersassertThat(budget.getIncome(), both(notNullValue()).and(is(1000.00)))

    Lists and Custom Matchers:

    List MatchersassertThat(budgets, hasItem(negativeSpendingBudget))Custom MatchersassertThat(negativeSpendingBudget, hasIncomeOf(1000.00))

  • Matchers Custom MatchersExtend Matcher implementation

    public class HasIncomeOf extends BaseMatcher

    Matcher implementations: BaseMatcher (Hamcrest) TypeSafetyMatcher (JUnit Matcher extends BaseMatcher) CombinableMatcher (JUnit Matcher extends BaseMatcher) SubstringMatcher (JUnit Matcher extends TypeSafeMatcher)

    Create a static convenience factory method for the matcher- factory method name is important as this is the static test method

    @Factorypublic static Matcher hasIncomeOf(Double income) { return new HasIncomeOf(income);}

    Implement the match method for the matcher implementation

    BaseMatcher = boolean matches(Object item)TypeSafetyMatcher = boolean matchesSafely(T item)

  • Matchers Custom MatchersImplement the describeTo(Description description) method to improve failure messages

    description.appendText("has income of ").appendValue(this.income)

    Import the static convenience factory method to use the matcher

    import static nz.co.solnetsolutions.custom.matchers.HasIncomeOf.hasIncomeOf assertThat(negativeSpendingBudget, hasIncomeOf(900.00));

  • The @Ignore AnnotationUse of this annotation means that there should be no need to comment out tests (when committing code)Can be used to temporarily disable a test or group of tests (during code refactoring, place holder)Can be used at the class level in this case no tests will be executedNative JUnit 4 test runners should report the number of ignored tests alongside tests run and failuresAn optional default parameter for recording the reason why a test/group of tests is being ignored

    @Ignore(Reason for ignoring this test!)

  • SummaryParameterized TestsUse to eliminate DRY test code smellsUse when writing data-driven tests or tests that require different data in different environmentsCreate generic tests that make use of a static factory method decorated with the @Parameters annotationCreate class members and constructor and decorate with the @RunWith annotationExtensible: Named Parameterized Test & XML Data Driven Parameterized TestMatcher AssertionsRead more naturally [ assertThat(a, is(3) ]More readable failure messages [ Expected is: 3 got: 2 ]Can use combinations of matchers [ assertThat(a, both(notNullValue()).and(is(3)))]List matchers [ assertThat(myList, hasItem(item1))Custom matchers@Ignore AnnotationNo more commented out tests!Temporarily disable tests or groups of testsNative JUnit runners will report the tests as ignoredGood practice to include the reason for the test being ignored in the @Ignore annotation

  • ReferencesGiudici, Fabrizio - JUnit: A Little Beyond @Test, @Before, @After (http://netbeans.dzone.com/articles/junit-little-beyond-test-after)Hamcrest (http://code.google.com/p/hamcrest/)JMock (http://www.jmock.org)JUnit release notes (http://junit.sourceforge.net/doc/ReleaseNotes4.4.html)JUnit APIsJUext (http://junitext.sourceforge.net/)Test Early Blog (http://www.testearly.com)Walnes, Joe Flexible JUnit assertions with assertThat() (http://joe.truemesh.com/blog/000511.html)