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 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:
- full control on mocked out result. It is possible to test such a scenarios which are extremely rare.
- high speed of execution because we do not need to call a real (in this case database) implementation.
QUnit - JavaScript unit test
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:
- open the
test.html
in the browser. - User browser plugin or some kind of bookmarklet to automatically refresh page
- Write
test.js
- After saving file the result will be reflected in the browser after the next refresh.
Technology stack
- Physical software project structure
- Service layer
- Data access layer
- Spring MVC
- Web view
- Toolbox
- Testing
- Jawr, webjars, bootstrap, Spring setup trick
- Jakarta Equivalence Relation
- Difficult to test example refactoring