Testing

Group unit tests

JUnit freamwork is simple. It does not differentiate whether you are developing “real” unit test or integration test.

Another problem can be if certain test are just too slow to run after every change (In my current - lagacy - system it takes about 45 minutes to complete all unit test).

So make categorization. But the default categorization is extramely verbose. You want to make categorization automatic. Well, in real it is not about automatic categorization but about automatically selecting which test to run in the “slow” and which one in the “fast” test suite. Of course there are many-many way of grouping tests.

The following code is from Andreas Hochsteger. I just republish the source code because webpages are just wanishing.

  1/** This category marks slow tests. */
  2public interface SlowTestCategory {
  3}
  4
  5/** This category marks tests that require an internet connection. */
  6public interface OnlineTestCategory {
  7}
  8
  9public class SampleTest {
 10 @Test
 11 @Category({OnlineTestCategory.class, SlowTestCategory.class})
 12 public void onlineAndSlowTestCategoryMethod() {
 13 }
 14
 15 @Test
 16 @Category(OnlineTestCategory.class)
 17 public void onlineTestCategoryMethod() {
 18 }
 19
 20 @Test
 21 @Category(SlowTestCategory.class)
 22 public void slowTestCategoryMethod() {
 23 }
 24
 25 @Test
 26 public void noTestCategoryMethod() {
 27 }
 28}
 29
 30/** MyTestSuite runs all slow tests, excluding all test which require a network connection. */
 31@RunWith(FlexibleCategories.class)
 32@ExcludeCategory(OnlineTestCategory.class)
 33@IncludeCategory(SlowTestCategory.class)
 34@TestScanPackage("my.package")
 35@TestClassPrefix("")
 36@TestClassSuffix("Test")
 37public class MyTestSuite {
 38}
 39
 40// ------------------- FlexibleCategories.java  ----------------
 41
 42import java.lang.annotation.Annotation;
 43import java.lang.annotation.Retention;
 44import java.lang.annotation.RetentionPolicy;
 45
 46import org.junit.Test;
 47import org.junit.experimental.categories.Categories.CategoryFilter;
 48import org.junit.experimental.categories.Categories.ExcludeCategory;
 49import org.junit.experimental.categories.Categories.IncludeCategory;
 50import org.junit.experimental.categories.Category;
 51import org.junit.runner.Description;
 52import org.junit.runner.manipulation.NoTestsRemainException;
 53import org.junit.runners.Suite;
 54import org.junit.runners.model.InitializationError;
 55import org.junit.runners.model.RunnerBuilder;
 56
 57/**
 58 * This class is based on org.junit.experimental.categories.Categories from JUnit 4.10.
 59 *
 60 * All anotations and inner classes from the original class Categories are removed,
 61 * since they will be re-used.
 62 * Unfortunately sub-classing Categories did not work.
 63 */
 64public class FlexibleCategories extends Suite {
 65
 66 /**
 67  * Specifies the package which should be scanned for test classes (e.g. @TestScanPackage("my.package")).
 68  * This annotation is required.
 69  */
 70 @Retention(RetentionPolicy.RUNTIME)
 71 public @interface TestScanPackage {
 72  public String value();
 73 }
 74
 75 /**
 76  * Specifies the prefix of matching class names (e.g. @TestClassPrefix("Test")).
 77  * This annotation is optional (default: "").
 78  */
 79 @Retention(RetentionPolicy.RUNTIME)
 80 public @interface TestClassPrefix {
 81  public String value();
 82 }
 83
 84 /**
 85  * Specifies the suffix of matching class names (e.g. @TestClassSuffix("Test")).
 86  * This annotation is optional (default: "Test").
 87  */
 88 @Retention(RetentionPolicy.RUNTIME)
 89 public @interface TestClassSuffix {
 90  public String value();
 91 }
 92
 93 /**
 94  * Specifies an annotation for methods which must be present in a matching class (e.g. @TestMethodAnnotationFilter(Test.class)).
 95  * This annotation is optional (default: org.junit.Test.class).
 96  */
 97 @Retention(RetentionPolicy.RUNTIME)
 98 public @interface TestMethodAnnotation {
 99  public Class<? extends Annotation> value();
100 }
101
102 public FlexibleCategories(Class<?> clazz, RunnerBuilder builder)
103   throws InitializationError {
104  this(builder, clazz, PatternClasspathClassesFinder.getSuiteClasses(
105    getTestScanPackage(clazz), getTestClassPrefix(clazz), getTestClassSuffix(clazz),
106    getTestMethodAnnotation(clazz)));
107  try {
108   filter(new CategoryFilter(getIncludedCategory(clazz),
109     getExcludedCategory(clazz)));
110  } catch (NoTestsRemainException e) {
111   // Ignore all classes with no matching tests.
112  }
113  assertNoCategorizedDescendentsOfUncategorizeableParents(getDescription());
114 }
115
116 public FlexibleCategories(RunnerBuilder builder, Class<?> clazz,
117   Class<?>[] suiteClasses) throws InitializationError {
118  super(builder, clazz, suiteClasses);
119 }
120
121 private static String getTestScanPackage(Class<?> clazz) throws InitializationError {
122  TestScanPackage annotation = clazz.getAnnotation(TestScanPackage.class);
123  if (annotation == null) {
124   throw new InitializationError("No package given to scan for tests!\nUse the annotation @TestScanPackage(\"my.package\") on the test suite " + clazz + ".");
125  }
126  return annotation.value();
127 }
128
129 private static String getTestClassPrefix(Class<?> clazz) {
130  TestClassPrefix annotation = clazz.getAnnotation(TestClassPrefix.class);
131  return annotation == null ? "" : annotation.value();
132 }
133
134 private static String getTestClassSuffix(Class<?> clazz) {
135  TestClassSuffix annotation = clazz.getAnnotation(TestClassSuffix.class);
136  return annotation == null ? "Test" : annotation.value();
137 }
138
139 private static Class<? extends Annotation> getTestMethodAnnotation(Class<?> clazz) {
140  TestMethodAnnotation annotation = clazz.getAnnotation(TestMethodAnnotation.class);
141  return annotation == null ? Test.class : annotation.value();
142 }
143
144 private Class<?> getIncludedCategory(Class<?> clazz) {
145  IncludeCategory annotation= clazz.getAnnotation(IncludeCategory.class);
146  return annotation == null ? null : annotation.value();
147 }
148
149 private Class<?> getExcludedCategory(Class<?> clazz) {
150  ExcludeCategory annotation= clazz.getAnnotation(ExcludeCategory.class);
151  return annotation == null ? null : annotation.value();
152 }
153
154 private void assertNoCategorizedDescendentsOfUncategorizeableParents(Description description) throws InitializationError {
155  if (!canHaveCategorizedChildren(description))
156   assertNoDescendantsHaveCategoryAnnotations(description);
157  for (Description each : description.getChildren())
158   assertNoCategorizedDescendentsOfUncategorizeableParents(each);
159 }
160
161 private void assertNoDescendantsHaveCategoryAnnotations(Description description) throws InitializationError {
162  for (Description each : description.getChildren()) {
163   if (each.getAnnotation(Category.class) != null)
164    throw new InitializationError("Category annotations on Parameterized classes are not supported on individual methods.");
165   assertNoDescendantsHaveCategoryAnnotations(each);
166  }
167 }
168
169 // If children have names like [0], our current magical category code can't determine their
170 // parentage.
171 private static boolean canHaveCategorizedChildren(Description description) {
172  for (Description each : description.getChildren())
173   if (each.getTestClass() == null)
174    return false;
175  return true;
176 }
177}
178
179PatternClasspathClassesFinder.java
180
181import java.io.File;
182import java.io.IOException;
183import java.lang.annotation.Annotation;
184import java.lang.reflect.Method;
185import java.net.URL;
186import java.util.ArrayList;
187import java.util.Enumeration;
188import java.util.List;
189
190/**
191 *
192 * Modified version of ClasspathClassesFinder from:
193 * http://linsolas.free.fr/wordpress/index.php/2011/02/how-to-categorize-junit-tests-with-maven/
194 *
195 * The difference is, that it does not search for annotated classes but for classes with a certain
196 * class name prefix and suffix.
197 */
198public final class PatternClasspathClassesFinder {
199
200 /**
201  * Get the list of classes of a given package name, and that are annotated
202  * by a given annotation.
203  *
204  * @param packageName
205  *            The package name of the classes.
206  * @param classPrefix
207  *            The prefix of the class name.
208  * @param classSuffix
209  *            The suffix of the class name.
210  * @param methodAnnotation
211  *            Only return classes containing methods annotated with methodAnnotation.
212  * @return The List of classes that matches the requirements.
213  */
214 public static Class<?>[] getSuiteClasses(String packageName,
215   String classPrefix, String classSuffix,
216   Class<? extends Annotation> methodAnnotation) {
217  try {
218   return getClasses(packageName, classPrefix, classSuffix, methodAnnotation);
219  } catch (Exception e) {
220   e.printStackTrace();
221  }
222  return null;
223 }
224
225 /**
226  * Get the list of classes of a given package name, and that are annotated
227  * by a given annotation.
228  *
229  * @param packageName
230  *            The package name of the classes.
231  * @param classPrefix
232  *            The prefix of the class name.
233  * @param classSuffix
234  *            The suffix of the class name.
235  * @param methodAnnotation
236  *            Only return classes containing methods annotated with methodAnnotation.
237  * @return The List of classes that matches the requirements.
238  * @throws ClassNotFoundException
239  *             If something goes wrong...
240  * @throws IOException
241  *             If something goes wrong...
242  */
243 private static Class<?>[] getClasses(String packageName,
244   String classPrefix, String classSuffix,
245   Class<? extends Annotation> methodAnnotation)
246   throws ClassNotFoundException, IOException {
247  ClassLoader classLoader = Thread.currentThread()
248    .getContextClassLoader();
249  String path = packageName.replace('.', '/');
250  // Get classpath
251  Enumeration<URL> resources = classLoader.getResources(path);
252  List<File> dirs = new ArrayList<File>();
253  while (resources.hasMoreElements()) {
254   URL resource = resources.nextElement();
255   dirs.add(new File(resource.getFile()));
256  }
257  // For each classpath, get the classes.
258  ArrayList<Class<?>> classes = new ArrayList<Class<?>>();
259  for (File directory : dirs) {
260   classes.addAll(findClasses(directory, packageName, classPrefix, classSuffix, methodAnnotation));
261  }
262  return classes.toArray(new Class[classes.size()]);
263 }
264
265 /**
266  * Find classes, in a given directory (recursively), for a given package
267  * name, that are annotated by a given annotation.
268  *
269  * @param directory
270  *            The directory where to look for.
271  * @param packageName
272  *            The package name of the classes.
273  * @param classPrefix
274  *            The prefix of the class name.
275  * @param classSuffix
276  *            The suffix of the class name.
277  * @param methodAnnotation
278  *            Only return classes containing methods annotated with methodAnnotation.
279  * @return The List of classes that matches the requirements.
280  * @throws ClassNotFoundException
281  *             If something goes wrong...
282  */
283 private static List<Class<?>> findClasses(File directory,
284   String packageName, String classPrefix, String classSuffix,
285   Class<? extends Annotation> methodAnnotation)
286   throws ClassNotFoundException {
287  List<Class<?>> classes = new ArrayList<Class<?>>();
288  if (!directory.exists()) {
289   return classes;
290  }
291  File[] files = directory.listFiles();
292  for (File file : files) {
293   if (file.isDirectory()) {
294    classes.addAll(findClasses(file,
295      packageName + "." + file.getName(), classPrefix, classSuffix, methodAnnotation));
296   } else if (file.getName().startsWith(classPrefix) && file.getName().endsWith(classSuffix + ".class")) {
297    // We remove the .class at the end of the filename to get the
298    // class name...
299    Class<?> clazz = Class.forName(packageName
300      + '.'
301      + file.getName().substring(0,
302        file.getName().length() - 6));
303
304    // Check, if class contains test methods (prevent "No runnable methods" exception):
305    boolean classHasTest = false;
306    for (Method method : clazz.getMethods()) {
307     if (method.getAnnotation(methodAnnotation) != null) {
308      classHasTest = true;
309      break;
310     }
311    }
312    if (classHasTest) {
313     classes.add(clazz);
314    }
315   }
316  }
317  return classes;
318 }
319}

Enclosed tests

The Enclosed suite also gives a good option to organize Test classes.

 1@RunWith(Enclosed.class)
 2public class TeamReserveDialogTest {
 3  // shered test helpers
 4  
 5  public static class DegenerateTest{
 6        // having ts own test init and tead down methods if needed
 7  }
 8  
 9  public static class NewTeamReserveDialogTest extends ATeamReserveDialogTest {
10  }
11
12  public static class TeamReservePropertiesDialogTest extends ATeamReserveDialogTest {
13  }
14
15  @Ignore
16  public static abstract class ATeamReserveDialogTest {
17        // common inherited test functions
18    @Test
19    public void test() {
20      fail("Not yet implemented");
21    }
22
23  }
24}
25

Mockito

mockito

Mockito is a mocking framework that tastes really good. It lets you write beautiful tests with clean & simple API.

In practice: Imagine when you want to test a business logic which is relying on database access. Reading from database. to test such a code you need to mock out that part of the system because database behavior can be indeterministic. Mock frameworks are good example for that.

Example use in PLI:

 1public class SessionFinderTest {
 2  String username = "SessionFinderTest";
 3  SessionFinder finder;
 4  IPliSessionDao pliSessionDao;
 5
 6  @Before
 7  public void before() {
 8          finder = new SessionFinder();
 9          ReflectionTestUtils.setField(finder, "pliSessionDao", pliSessionDao.          mock(IPliSessionDao.class));
10          when(pliSessionDao.searchBy(any(SessionSearchCriteria.class))).thenReturn(
11          asList(SessionDto(1L, 10), SessionDto(2L, 10), SessionDto(3L, 10)));
12          ...
13          ListMultimap<Long, DateRange> shifts = ...
14          when(pliSessionDao.findShifts(asList(1L, 2L, 3L))).thenReturn(shifts);
15          ...
16          Map<Pair<Long, DateRange>, List<LanguageCoverageInfo>> lci = ....
17          when(pliSessionDao.getLanguageCoverageInformation(username, shifts)).thenReturn(lci);
18  }
19
20  @Test
21  public void testFind() {
22        PagedResult<SessionDto> res = finder.find(username, new SessionSearchCriteria());
23        Assert.assertEquals(10, res.getCounter());
24        Assert.assertEquals(Long.valueOf(1), res.getResult().get(0).getSess_uid());
25        Assert.assertEquals(1, res.getResult().get(0).getLang_coverage_info().size());
26  }
27}

Advantages:

QUnit - JavaScript unit test

QUnit

Setup unit test page:

 1        <link rel="stylesheet" href="../assets/unittest/qunit.css">
 2        <script src="../assets/unittest/qunit.js"></script>
 3        <script src="../assets/jquery/js/jquery-1.8.2.js"></script>
 4        <script src="../assets/underscore/js/underscore.js"></script>
 5        <script src="../assets/amplify/js/amplify.js"></script>
 6        <script src="../assets/app/js/message-bus.js"></script>
 7        <script src="../assets/app/js/booth.js"></script>
 8        <script src="../assets/app/js/session.js"></script>
 9        <script src="test.js"></script>
10        </head>
11        <body>
12          <div id="qunit"></div>
13          <div id="qunit-fixture">test markup</div>
14        </body>

Then write unit test in JavaScript

 1        test("hello test", function() {
 2          ok(1 == "1", "Passed!");
 3        });
 4
 5        // before this you need som setup code - see documentation
 6        test('booth.findInterpByAssignmentLineUid not found', function() {
 7          var interp = booth.findInterpByAssignmentLineUid(-1);
 8          ok(_.isNull(interp));
 9        });
10
11        test('booth.findInterpByAssignmentLineUid found', function() {
12          var interp = booth.findInterpByAssignmentLineUid(1);
13          ok(interp);
14          equal('should find',interp.name);
15        });

EP Eclipse distribution prevent us installing plugins which can help to run these test in Eclipse while developing.

But there are solutions:

  1. open the test.html in the browser.
  2. User browser plugin or some kind of bookmarklet to automatically refresh page
  3. Write test.js
  4. After saving file the result will be reflected in the browser after the next refresh.

Technology stack
Jul 24, 2013
comments powered by Disqus

Links

Cool

RSS