001/** 002 * The MIT License (MIT) 003 * 004 * Copyright (c) 2017 tools4j.org (Marco Terzer) 005 * 006 * Permission is hereby granted, free of charge, to any person obtaining a copy 007 * of this software and associated documentation files (the "Software"), to deal 008 * in the Software without restriction, including without limitation the rights 009 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 010 * copies of the Software, and to permit persons to whom the Software is 011 * furnished to do so, subject to the following conditions: 012 * 013 * The above copyright notice and this permission notice shall be included in all 014 * copies or substantial portions of the Software. 015 * 016 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 017 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 018 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 019 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 020 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 021 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 022 * SOFTWARE. 023 */ 024package org.tools4j.spockito; 025 026import org.junit.Assert; 027import org.junit.Test; 028import org.junit.runner.Runner; 029import org.junit.runner.manipulation.Filter; 030import org.junit.runner.manipulation.NoTestsRemainException; 031import org.junit.runners.Suite; 032import org.junit.runners.model.FrameworkMethod; 033import org.junit.runners.model.InitializationError; 034import org.junit.runners.model.TestClass; 035 036import java.lang.annotation.*; 037import java.lang.reflect.Constructor; 038import java.lang.reflect.Executable; 039import java.lang.reflect.Parameter; 040import java.text.MessageFormat; 041import java.util.ArrayList; 042import java.util.List; 043 044/** 045 * The custom runner <code>Spockito</code> implements parameterized tests where the test data 046 * is defined in a table-like structure via {@link Unroll} annotation. 047 */ 048public class Spockito extends Suite { 049 050 private static final String DEFAULT_NAME = "[{row}]: {0}"; 051 052 /** 053 * Annotation for a test or a test method which provides test data for the tests. If the 054 * annotation made at the test class level, then the test data is applied to all test 055 * methods. Test data on method level is applied for that method only. 056 */ 057 @Retention(RetentionPolicy.RUNTIME) 058 @Target(value = {ElementType.TYPE, ElementType.METHOD}) 059 public @interface Unroll { 060 /** 061 * Annotation with test case values declared in a table like structure as follows: 062 * <pre> 063 * | ColumnA | ColumnB | ColumnC | 064 * |-----------|-----------|-----------| 065 * | value_1_A | value_1_B | value_1_C | 066 * | value_2_A | value_2_B | value_2_C | 067 * etc... 068 * </pre> 069 * The separator row after the column headers is optional and = instead of - can be used. Separator rows can be 070 * placed anywhere in the table and are ignored when the table is parsed. 071 * 072 * @return An array of strings represented as table data; string[0] contains the header 073 * row with column names 074 * @see MessageFormat 075 */ 076 String[] value(); 077 } 078 079 /** 080 * Annotation for a test or a test method to indicate an alternative name for the parameterized 081 * test. Default name is "[{row}]: {0}". 082 */ 083 @Retention(RetentionPolicy.RUNTIME) 084 @Target(value = {ElementType.TYPE, ElementType.METHOD}) 085 public @interface Name { 086 /** 087 * Optional pattern to derive the test's name from the parameters. Use 088 * numbers in braces to refer to the parameters or the additional data 089 * as follows: 090 * <pre> 091 * {row} - the current row index (zero based) 092 * {0} - the row's value in the first column 093 * {1} - the row's value in the second column 094 * {ColumnA} - the row's value in the "ColumnA" column 095 * {ColumnB} - the row's value in the "ColumnB" column 096 * etc... 097 * </pre> 098 * <p> 099 * Default value is "[{row}]: {0}". 100 * 101 * @return A pattern string a bit similar to {@link MessageFormat} 102 */ 103 String value() default DEFAULT_NAME; 104 105 /** 106 * Returns true if the SHORT name format shall be used instead of the default LONG name format. 107 * <p> 108 * LONG and SHORT name formats are defined as follows: 109 * <pre> 110 * false (LONG format): {@literal "<TestClass>.<TestMethod><Name>"} 111 * true (SHORT format): {@literal "<Name>"} for method level unrolling and 112 * {@literal "<TestMethod>"} or {@literal "<TestMethod><Name>"} for class level unrolling 113 * </pre> 114 * SHORT format is much nicer to look at but unfortunately it prevents individual test from being re-run in 115 * Intellij. 116 * 117 * @return true if SHORT format shall be used, false by default indicating LONG format 118 */ 119 boolean shortFormat() default false; 120 } 121 122 /** 123 * Annotation for fields or parameters of a test method or of the test constructor. Fields 124 * need only be annotated if the field name differs from the column name of the test data. 125 * Constructor or test method parameters need to be annotated if they are not in the same 126 * order as the columns in the test data. 127 * <p> 128 * The following reference types are supported: 129 * <pre> 130 * {row} - the current row index (zero based), assignable to an integer type 131 * {*} - indicating that all rows are to be used, assignable to a collection type, a map or a Bean 132 * {ColumnA} - the value in the "ColumnA" column 133 * {ColumnB} - the value in the "ColumnB" column 134 * </pre> 135 */ 136 @Retention(RetentionPolicy.RUNTIME) 137 @Target(value = {ElementType.FIELD, ElementType.PARAMETER}) 138 public @interface Ref { 139 /** 140 * Returns the column name, or {row} for row index and {*} to map all column values to the annotated variable. 141 * Can be omitted when annotating fields and the field name is identical to the column name. 142 * 143 * @return the column name, or "{row}" for the row index, or "{*}" to indicate that all all column values should 144 * be mapped to the annotated variable (for list, map and bean types) 145 */ 146 String value() default ""; 147 } 148 149 /** 150 * Add this annotation to your test class or method if you want to specify custom value converters from string to 151 * typed parameters. The converter must have a public zero-arg constructor. 152 */ 153 @Retention(RetentionPolicy.RUNTIME) 154 @Inherited 155 @Target(value = {ElementType.TYPE, ElementType.METHOD, ElementType.CONSTRUCTOR}) 156 public @interface UseValueConverter { 157 /** 158 * @return a {@link ValueConverter} class (must have a default constructor) 159 */ 160 Class<? extends ValueConverter> value() default SpockitoValueConverter.class; 161 } 162 163 /** 164 * Only called reflectively. Do not use programmatically. 165 * @param clazz the test class 166 * @throws InitializationError when a problem occurs during the initialisation of the runner 167 */ 168 public Spockito(final Class<?> clazz) throws InitializationError { 169 super(clazz, createRunners(clazz)); 170 } 171 172 @Override 173 public void filter(final Filter filter) throws NoTestsRemainException { 174 super.filter(new MethodLevelFilter(filter)); 175 } 176 177 private static Table classWideTableOrNull(final Class<?> clazz) { 178 Unroll unroll = getOnlyConstructor(clazz).getAnnotation(Unroll.class); 179 if (unroll == null) { 180 unroll = clazz.getAnnotation(Unroll.class); 181 } 182 return unroll == null ? null : Table.parse(unroll.value()); 183 } 184 185 private static List<Runner> createRunners(final Class<?> clazz) throws InitializationError { 186 final ValueConverter defaultValueConverter = getDefaultValueConverter(clazz); 187 final List<Runner> runners = new ArrayList<>(); 188 final Table classWideTable = classWideTableOrNull(clazz); 189 if (classWideTable != null) { 190 for (final TableRow row : classWideTable) { 191 runners.add(new SingleRowMultiTestRunner(clazz, row, defaultValueConverter)); 192 } 193 } else { 194 for (final FrameworkMethod testMethod : new TestClass(clazz).getAnnotatedMethods(Test.class)) { 195 final Spockito.UseValueConverter useValueConverter = testMethod.getAnnotation(Spockito.UseValueConverter.class); 196 final ValueConverter methodValueConverter = Spockito.getValueConverter(useValueConverter, defaultValueConverter); 197 runners.add(new SingleTestMultiRowRunner(clazz, testMethod, methodValueConverter)); 198 } 199 } 200 return runners; 201 } 202 203 private static Constructor<?> getOnlyConstructor(final Class<?> clazz) { 204 Constructor<?>[] constructors = clazz.getConstructors(); 205 Assert.assertEquals(1, constructors.length); 206 return constructors[0]; 207 } 208 209 private static ValueConverter getDefaultValueConverter(final Class<?> clazz) { 210 final Spockito.UseValueConverter useValueConverter = clazz.getAnnotation(Spockito.UseValueConverter.class); 211 return Spockito.getValueConverter(useValueConverter, SpockitoValueConverter.DEFAULT_INSTANCE); 212 } 213 214 static String parameterRefNameOrNull(final Parameter parameter) { 215 final Spockito.Ref ref = parameter.getAnnotation(Spockito.Ref.class); 216 if (ref == null) { 217 return parameter.isNamePresent() ? parameter.getName() : null; 218 } else { 219 return ref.value(); 220 } 221 } 222 223 static Name nameAnnotationOrNull(final Executable executable) { 224 final Name name = executable.getAnnotation(Name.class); 225 return name != null ? name : executable.getDeclaringClass().getAnnotation(Name.class); 226 } 227 static String getName(final Executable executable, final TableRow tableRow) { 228 return getName(nameAnnotationOrNull(executable), tableRow, DEFAULT_NAME); 229 } 230 static String getName(final Name name, final TableRow tableRow, final String defaultName) { 231 final String unresolved = name != null ? name.value() : defaultName; 232 String resolved = unresolved; 233 resolved = resolved.replaceAll("\\{row\\}", String.valueOf(tableRow.getRowIndex())); 234 final Table table = tableRow.getTable(); 235 for (int col = 0; col < table.getColumnCount(); col++) { 236 final String value = tableRow.get(col); 237 resolved = resolved.replaceAll("\\{" + col + "\\}", value); 238 resolved = resolved.replaceAll("\\{" + table.getColumnName(col) + "\\}", value); 239 } 240 return resolved; 241 } 242 243 static ValueConverter getValueConverter(final Spockito.UseValueConverter useValueConverter, final ValueConverter defaultValueConverter) { 244 if (useValueConverter != null) { 245 try { 246 return useValueConverter.value().newInstance(); 247 } catch (final Exception e) { 248 throw new IllegalArgumentException("Could not instantiate ValueConverter of type " + useValueConverter.value(), e); 249 } 250 } 251 return defaultValueConverter; 252 } 253 254}