From 21e7f071533467b9b653faa91a899be8bfcc4c1f Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Wed, 1 Apr 2026 10:22:54 +0000 Subject: [PATCH] Add @WithNullParam, @WithEnumParams, @WithParamsSource annotations and asEnum() accessor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - @WithNullParam: runs the test twice — once with a given value, once with null - @WithEnumParams: iterates over all constants of an enum class automatically - @WithParamsSource: reads parameter rows from a static String[][] provider method - asEnum(Class) / asEnum(String, Class): type-safe enum accessor returning null for null inputs - Full Javadocs on all new public API - Comprehensive unit and integration tests for all new features - README updated with usage examples for each new annotation and accessor - ParameterizedStatement.evaluate() now dispatches to per-annotation handler methods - ParameterizedStatement constructor now accepts target instance (required for @WithParamsSource reflection) Closes #7 Co-authored-by: Ignacio Tomas Crespo --- readme.md | 123 ++++++ .../junitwithparams/WithEnumParams.java | 82 ++++ .../junitwithparams/WithNullParam.java | 78 ++++ .../junitwithparams/WithParamsRule.java | 356 ++++++++++++++++-- .../junitwithparams/WithParamsSource.java | 101 +++++ .../ParameterizedStatementTest.java | 2 +- .../junitwithparams/WithEnumParamsTest.java | 96 +++++ .../junitwithparams/WithNullParamTest.java | 89 +++++ .../junitwithparams/WithParamsRuleTest.java | 93 +++++ .../junitwithparams/WithParamsSourceTest.java | 135 +++++++ 10 files changed, 1115 insertions(+), 40 deletions(-) create mode 100644 src/main/java/com/github/ignaciotcrespo/junitwithparams/WithEnumParams.java create mode 100644 src/main/java/com/github/ignaciotcrespo/junitwithparams/WithNullParam.java create mode 100644 src/main/java/com/github/ignaciotcrespo/junitwithparams/WithParamsSource.java create mode 100644 src/test/java/com/github/ignaciotcrespo/junitwithparams/WithEnumParamsTest.java create mode 100644 src/test/java/com/github/ignaciotcrespo/junitwithparams/WithNullParamTest.java create mode 100644 src/test/java/com/github/ignaciotcrespo/junitwithparams/WithParamsSourceTest.java diff --git a/readme.md b/readme.md index 5d01535..d714c51 100644 --- a/readme.md +++ b/readme.md @@ -53,6 +53,129 @@ public class CalculatorTest { } ``` +## `@WithBooleanParams` — true / false shorthand + +Runs the test twice: once with `true` and once with `false`. No need to list the values manually. + +```java +@Test +@WithBooleanParams +public void toggleFeature() { + boolean enabled = params.asBoolean(); + feature.setEnabled(enabled); + assertEquals(enabled, feature.isEnabled()); +} +``` + +## `@WithNullParam` — null-safety testing + +Runs the test twice: once with the provided non-null value and once with `null`. +Ideal for verifying that your code handles `null` inputs gracefully. + +```java +@Test +@WithNullParam("hello") +public void handlesNull() { + String value = params.get(); // "hello" on the first run, null on the second + processValue(value); // must not throw for either case +} +``` + +Use the optional `name` attribute for named parameters: + +```java +@Test +@WithNullParam(value = "hello", name = "input") +public void handlesNullNamed() { + String value = params.get("input"); +} +``` + +## `@WithEnumParams` — iterate over all enum constants + +Automatically runs the test once for every constant in the specified enum class. +The current constant is retrieved via the type-safe `asEnum(Class)` accessor. + +```java +enum Color { RED, GREEN, BLUE } + +@Test +@WithEnumParams(Color.class) +public void testAllColors() { + Color color = params.asEnum(Color.class); + assertNotNull(color); + renderBackground(color); // called for RED, GREEN, and BLUE +} +``` + +Use the optional `name` attribute for named parameters: + +```java +@Test +@WithEnumParams(value = Color.class, name = "color") +public void testAllColorsNamed() { + Color color = params.asEnum("color", Color.class); + assertNotNull(color); +} +``` + +## `@WithParamsSource` — method-provided parameter sets + +Reads parameter sets from a static method in the test class. +Avoids large annotation arrays and keeps complex data sets as real Java code. + +The provider method must be `static`, accept no arguments, and return `String[][]` +where each inner array is one test iteration. The `names` attribute maps column +indices to parameter names. + +```java +@Test +@WithParamsSource(value = "provideNumbers", names = {"n1", "n2", "result"}) +public void sum() { + int n1 = params.asInt("n1"); + int n2 = params.asInt("n2"); + assertEquals(params.asInt("result"), calculator.sum(n1, n2)); +} + +static String[][] provideNumbers() { + return new String[][] { + {"1", "2", "3"}, + {"11", "-2", "9"} + }; +} +``` + +Single-parameter shorthand (default name `"param1"`): + +```java +@Test +@WithParamsSource("provideWords") +public void wordIsNotEmpty() { + String word = params.get(); + assertFalse(word.isEmpty()); +} + +static String[][] provideWords() { + return new String[][] { {"hello"}, {"world"}, {"foo"} }; +} +``` + +## `asEnum(Class)` — type-safe enum accessor + +Converts the current string parameter value to an enum constant by name. +Returns `null` if the stored value is `null` (e.g. when used with `@WithNullParam`). + +```java +// default parameter +Color color = params.asEnum(Color.class); + +// named parameter +Color color = params.asEnum("colorParam", Color.class); +``` + +The accessor works with any enum and complements the existing typed accessors +(`asInt()`, `asBoolean()`, `asDouble()`, etc.). + ## Kotlin Kotlin is supported, with some small differences. diff --git a/src/main/java/com/github/ignaciotcrespo/junitwithparams/WithEnumParams.java b/src/main/java/com/github/ignaciotcrespo/junitwithparams/WithEnumParams.java new file mode 100644 index 0000000..0d37d87 --- /dev/null +++ b/src/main/java/com/github/ignaciotcrespo/junitwithparams/WithEnumParams.java @@ -0,0 +1,82 @@ +/** + * The MIT License + *

+ * Copyright (c) 2017, Ignacio Tomas Crespo (itcrespo@gmail.com) + *

+ * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + *

+ * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + *

+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.github.ignaciotcrespo.junitwithparams; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Parameterised test annotation that automatically iterates over every constant of an + * enum class. This eliminates the need to list each enum value as a string in + * {@link WithParams}, and keeps tests resilient to future enum additions. + * + *

Each enum constant is stored by its {@link Enum#name()} and can be retrieved in + * the test body via {@link WithParamsRule#asEnum(Class)}. + * + *

Example

+ *
+ * enum Color { RED, GREEN, BLUE }
+ *
+ * {@literal @}Test
+ * {@literal @}WithEnumParams(Color.class)
+ * public void testAllColors() {
+ *     Color color = params.asEnum(Color.class);
+ *     assertNotNull(color);
+ * }
+ * 
+ * + *

Named-parameter example

+ *
+ * {@literal @}Test
+ * {@literal @}WithEnumParams(value = Color.class, name = "color")
+ * public void testAllColorsNamed() {
+ *     Color color = params.asEnum("color", Color.class);
+ * }
+ * 
+ * + * @see WithParamsRule#asEnum(Class) + * @see WithParamsRule#asEnum(String, Class) + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD}) +public @interface WithEnumParams { + + /** + * The enum class whose constants will be iterated. + * + * @return the enum class + */ + Class> value(); + + /** + * The parameter name used to retrieve the current constant in the test body via + * {@link WithParamsRule#asEnum(Class)} or {@link WithParamsRule#asEnum(String, Class)}. + * Defaults to {@code "param1"}. + * + * @return the parameter name + */ + String name() default "param1"; +} diff --git a/src/main/java/com/github/ignaciotcrespo/junitwithparams/WithNullParam.java b/src/main/java/com/github/ignaciotcrespo/junitwithparams/WithNullParam.java new file mode 100644 index 0000000..ed3ef78 --- /dev/null +++ b/src/main/java/com/github/ignaciotcrespo/junitwithparams/WithNullParam.java @@ -0,0 +1,78 @@ +/** + * The MIT License + *

+ * Copyright (c) 2017, Ignacio Tomas Crespo (itcrespo@gmail.com) + *

+ * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + *

+ * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + *

+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.github.ignaciotcrespo.junitwithparams; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Parameterised test annotation that runs the test twice: once with the provided + * non-null string value and once with {@code null}. + * + *

This is useful for null-safety testing — it ensures that code handles both a + * valid value and {@code null} without requiring you to write two separate tests. + * + *

Single-parameter example

+ *
+ * {@literal @}Test
+ * {@literal @}WithNullParam("hello")
+ * public void handlesNull() {
+ *     String value = params.get(); // "hello" on first run, null on second
+ *     // assert whatever makes sense for both cases
+ * }
+ * 
+ * + *

Named-parameter example

+ *
+ * {@literal @}Test
+ * {@literal @}WithNullParam(value = "hello", name = "input")
+ * public void handlesNullNamed() {
+ *     String value = params.get("input");
+ * }
+ * 
+ * + * @see WithParamsRule#get() + * @see WithBooleanParams + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD}) +public @interface WithNullParam { + + /** + * The non-null value to inject on the first test iteration. + * + * @return the string value + */ + String value(); + + /** + * The parameter name used to retrieve the value in the test body via + * {@link WithParamsRule#get(String)}. Defaults to {@code "param1"}. + * + * @return the parameter name + */ + String name() default "param1"; +} diff --git a/src/main/java/com/github/ignaciotcrespo/junitwithparams/WithParamsRule.java b/src/main/java/com/github/ignaciotcrespo/junitwithparams/WithParamsRule.java index ca2aeac..c6e70cb 100644 --- a/src/main/java/com/github/ignaciotcrespo/junitwithparams/WithParamsRule.java +++ b/src/main/java/com/github/ignaciotcrespo/junitwithparams/WithParamsRule.java @@ -27,6 +27,8 @@ import org.junit.runners.model.FrameworkMethod; import org.junit.runners.model.Statement; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; @@ -35,7 +37,33 @@ import java.util.Set; /** - * Created by crespo on 4/15/17. + * JUnit {@link MethodRule} that enables parameterised tests using annotations instead of + * a custom runner. Because it is a rule rather than a runner, it is compatible with any + * existing JUnit runner (Spring, Robolectric, Mockito, AndroidJUnit4, etc.). + * + *

Supported annotations

+ * + * + *

Quick start

+ *
+ * public class CalculatorTest {
+ *
+ *     {@literal @}Rule
+ *     public WithParamsRule params = new WithParamsRule();
+ *
+ *     {@literal @}Test
+ *     {@literal @}WithParams({"2", "4", "8", "1000"})
+ *     public void isEven() {
+ *         assertTrue(calculator.isEven(params.asInt()));
+ *     }
+ * }
+ * 
*/ public class WithParamsRule implements MethodRule { @@ -52,9 +80,26 @@ public class WithParamsRule implements MethodRule { @Override public Statement apply(final Statement base, final FrameworkMethod method, final Object target) { - return new ParameterizedStatement(executedTests, paramsMap, method, base, errorCollector, usedParams); + return new ParameterizedStatement(executedTests, paramsMap, method, base, errorCollector, usedParams, target); } + /** + * Returns the value of the default parameter ({@code "param1"}) as a {@link String}. + * + * @return the current parameter value, or {@code null} if {@code null} was injected + * @throws RuntimeException if no parameter named {@code "param1"} exists + */ + public String get() { + return get(PARAM_DEFAULT); + } + + /** + * Returns the value of the named parameter as a {@link String}. + * + * @param name the parameter name + * @return the current parameter value, or {@code null} if {@code null} was injected + * @throws RuntimeException if no parameter with the given name exists + */ public String get(final String name) { if (paramsMap.containsKey(name)) { String value = paramsMap.get(name); @@ -64,59 +109,183 @@ public String get(final String name) { throw new RuntimeException("Can't find parameter '" + name + "'"); } - public String get() { - return get(PARAM_DEFAULT); + /** + * Returns the value of the default parameter parsed as an {@code int}. + * + * @return the current parameter value as an int + * @throws NumberFormatException if the value cannot be parsed + */ + public int asInt() { + return asInt(PARAM_DEFAULT); } + /** + * Returns the value of the named parameter parsed as an {@code int}. + * + * @param name the parameter name + * @return the current parameter value as an int + * @throws NumberFormatException if the value cannot be parsed + */ public int asInt(final String name) { return Integer.parseInt(get(name)); } - public int asInt() { - return asInt(PARAM_DEFAULT); + /** + * Returns the value of the default parameter parsed as a {@code long}. + * + * @return the current parameter value as a long + * @throws NumberFormatException if the value cannot be parsed + */ + public long asLong() { + return asLong(PARAM_DEFAULT); } + /** + * Returns the value of the named parameter parsed as a {@code long}. + * + * @param name the parameter name + * @return the current parameter value as a long + * @throws NumberFormatException if the value cannot be parsed + */ public long asLong(final String name) { return Long.parseLong(get(name)); } - public long asLong() { - return asLong(PARAM_DEFAULT); + /** + * Returns the value of the default parameter parsed as a {@code double}. + * + * @return the current parameter value as a double + * @throws NumberFormatException if the value cannot be parsed + */ + public double asDouble() { + return asDouble(PARAM_DEFAULT); } + /** + * Returns the value of the named parameter parsed as a {@code double}. + * + * @param name the parameter name + * @return the current parameter value as a double + * @throws NumberFormatException if the value cannot be parsed + */ public double asDouble(final String name) { return Double.parseDouble(get(name)); } - public double asDouble() { - return asDouble(PARAM_DEFAULT); + /** + * Returns the value of the default parameter parsed as a {@code float}. + * + * @return the current parameter value as a float + * @throws NumberFormatException if the value cannot be parsed + */ + public float asFloat() { + return asFloat(PARAM_DEFAULT); } + /** + * Returns the value of the named parameter parsed as a {@code float}. + * + * @param name the parameter name + * @return the current parameter value as a float + * @throws NumberFormatException if the value cannot be parsed + */ public float asFloat(final String name) { return Float.parseFloat(get(name)); } - public float asFloat() { - return asFloat(PARAM_DEFAULT); + /** + * Returns the value of the default parameter parsed as a {@code boolean}. + * Any value other than {@code "true"} (case-insensitive) returns {@code false}. + * + * @return the current parameter value as a boolean + */ + public boolean asBoolean() { + return asBoolean(PARAM_DEFAULT); } + /** + * Returns the value of the named parameter parsed as a {@code boolean}. + * Any value other than {@code "true"} (case-insensitive) returns {@code false}. + * + * @param name the parameter name + * @return the current parameter value as a boolean + */ public boolean asBoolean(final String name) { return Boolean.parseBoolean(get(name)); } - public boolean asBoolean() { - return asBoolean(PARAM_DEFAULT); - } - + /** + * Converts the value of the default parameter using the provided {@link Transform}. + * + * @param the target type + * @param transform the conversion function + * @return the transformed value + * @throws Exception if the transform throws + */ public T as(Transform transform) throws Exception { return as(PARAM_DEFAULT, transform); } + /** + * Converts the value of the named parameter using the provided {@link Transform}. + * + * @param the target type + * @param name the parameter name + * @param transform the conversion function + * @return the transformed value + * @throws Exception if the transform throws + */ public T as(String name, Transform transform) throws Exception { return transform.to(get(name)); } + /** + * Returns the value of the default parameter as an enum constant. + * The stored string must match the {@link Enum#name()} of one of the constants. + * + *

Returns {@code null} if {@code null} was injected (e.g. via {@link WithNullParam}). + * + * @param the enum type + * @param enumClass the enum class + * @return the matching enum constant, or {@code null} + * @throws IllegalArgumentException if the value does not match any constant name + */ + public > E asEnum(Class enumClass) { + return asEnum(PARAM_DEFAULT, enumClass); + } + + /** + * Returns the value of the named parameter as an enum constant. + * The stored string must match the {@link Enum#name()} of one of the constants. + * + *

Returns {@code null} if {@code null} was injected (e.g. via {@link WithNullParam}). + * + * @param the enum type + * @param name the parameter name + * @param enumClass the enum class + * @return the matching enum constant, or {@code null} + * @throws IllegalArgumentException if the value does not match any constant name + */ + public > E asEnum(String name, Class enumClass) { + String value = get(name); + if (value == null) { + return null; + } + return Enum.valueOf(enumClass, value); + } + + /** + * Functional interface for custom type conversions used with {@link #as(Transform)}. + * + * @param the target type + */ public interface Transform { + /** + * Converts a string parameter value to the target type. + * + * @param from the raw string value + * @return the converted value + */ T to(String from); } @@ -128,48 +297,157 @@ static class ParameterizedStatement extends Statement { private Set mExecutedTests; private HashMap mParamsMap; private HashSet usedParams; + private final Object mTarget; ParameterizedStatement(final Set executedTests, final HashMap paramsMap, final FrameworkMethod method, final Statement base, final ErrorCollector errorCollector, - HashSet usedParams) { + HashSet usedParams, Object target) { mMethod = method; mBase = base; this.mExecutedTests = executedTests; this.mParamsMap = paramsMap; this.errorCollector = errorCollector; this.usedParams = usedParams; + this.mTarget = target; } @Override public void evaluate() throws Throwable { - WithParams annotation; WithBooleanParams booleanParams = mMethod.getAnnotation(WithBooleanParams.class); + WithNullParam nullParam = mMethod.getAnnotation(WithNullParam.class); + WithEnumParams enumParams = mMethod.getAnnotation(WithEnumParams.class); + WithParamsSource paramsSource = mMethod.getAnnotation(WithParamsSource.class); + WithParams annotation = mMethod.getAnnotation(WithParams.class); + if (booleanParams != null) { - annotation = createBooleanParamsAnnotation(); + evaluateWithParams(createBooleanParamsAnnotation()); + } else if (nullParam != null) { + evaluateWithNullParam(nullParam); + } else if (enumParams != null) { + evaluateWithEnumParams(enumParams); + } else if (paramsSource != null) { + evaluateWithParamsSource(paramsSource); + } else if (annotation != null) { + evaluateWithParams(annotation); } else { - annotation = mMethod.getAnnotation(WithParams.class); + mBase.evaluate(); } - if (annotation != null) { - checkDuplicated(); - checkParameters(annotation); - Iterator values = Arrays.asList(annotation.value()).iterator(); - Iterator names = prepareToExecute(annotation); - int tests = 0; - while (values.hasNext()) { - mParamsMap.put(names.next(), values.next()); - if (noMore(names)) { - prepareUsedParams(); - executeTest(); - checkUsedParams(); - names = prepareToExecute(annotation); - tests++; - } + } + + private void evaluateWithParams(final WithParams annotation) throws Throwable { + checkDuplicated(); + checkParameters(annotation); + Iterator values = Arrays.asList(annotation.value()).iterator(); + Iterator names = prepareToExecute(annotation); + int tests = 0; + while (values.hasNext()) { + mParamsMap.put(names.next(), values.next()); + if (noMore(names)) { + prepareUsedParams(); + executeTest(); + checkUsedParams(); + names = prepareToExecute(annotation); + tests++; } - System.out.println("-- Passed " + (tests - errorCollector.getErrors()) + " of " + tests + " tests --\n"); - errorCollector.verify(); - } else { - mBase.evaluate(); } + System.out.println("-- Passed " + (tests - errorCollector.getErrors()) + " of " + tests + " tests --\n"); + errorCollector.verify(); + } + + private void evaluateWithNullParam(final WithNullParam annotation) throws Throwable { + checkDuplicated(); + String paramName = annotation.name(); + int errorsBefore = errorCollector.getErrors(); + + // Iteration 1: provided non-null value + mParamsMap.clear(); + mParamsMap.put(paramName, annotation.value()); + prepareUsedParams(); + executeTest(); + checkUsedParams(); + + // Iteration 2: null value + mParamsMap.clear(); + mParamsMap.put(paramName, null); + prepareUsedParams(); + executeTest(); + checkUsedParams(); + + int tests = 2; + int errors = errorCollector.getErrors() - errorsBefore; + System.out.println("-- Passed " + (tests - errors) + " of " + tests + " tests --\n"); + errorCollector.verify(); + } + + private void evaluateWithEnumParams(final WithEnumParams annotation) throws Throwable { + checkDuplicated(); + String paramName = annotation.name(); + Enum[] constants = annotation.value().getEnumConstants(); + int tests = 0; + int errorsBefore = errorCollector.getErrors(); + + for (Enum constant : constants) { + mParamsMap.clear(); + mParamsMap.put(paramName, constant.name()); + prepareUsedParams(); + executeTest(); + checkUsedParams(); + tests++; + } + + int errors = errorCollector.getErrors() - errorsBefore; + System.out.println("-- Passed " + (tests - errors) + " of " + tests + " tests --\n"); + errorCollector.verify(); + } + + private void evaluateWithParamsSource(final WithParamsSource annotation) throws Throwable { + checkDuplicated(); + String methodName = annotation.value(); + String[] names = annotation.names(); + + Class targetClass = mTarget.getClass(); + Method sourceMethod; + try { + sourceMethod = targetClass.getDeclaredMethod(methodName); + } catch (NoSuchMethodException e) { + throw new WithParamsException("Source method '" + methodName + "()' not found in " + + targetClass.getName()); + } + + if (!Modifier.isStatic(sourceMethod.getModifiers())) { + throw new WithParamsException("Source method '" + methodName + "()' must be static"); + } + sourceMethod.setAccessible(true); + + String[][] rows; + try { + rows = (String[][]) sourceMethod.invoke(null); + } catch (Exception e) { + throw new WithParamsException("Failed to invoke source method '" + methodName + "()': " + + e.getMessage()); + } + + int tests = 0; + int errorsBefore = errorCollector.getErrors(); + + for (String[] row : rows == null ? new String[0][] : rows) { + if (row.length != names.length) { + throw new WithParamsException("Row at index " + tests + " has " + row.length + + " element(s) but " + names.length + " name(s) were declared"); + } + mParamsMap.clear(); + for (int i = 0; i < names.length; i++) { + mParamsMap.put(names[i], row[i]); + } + prepareUsedParams(); + executeTest(); + checkUsedParams(); + tests++; + } + + int errors = errorCollector.getErrors() - errorsBefore; + System.out.println("-- Passed " + (tests - errors) + " of " + tests + " tests --\n"); + errorCollector.verify(); } @VisibleForTesting diff --git a/src/main/java/com/github/ignaciotcrespo/junitwithparams/WithParamsSource.java b/src/main/java/com/github/ignaciotcrespo/junitwithparams/WithParamsSource.java new file mode 100644 index 0000000..b65208f --- /dev/null +++ b/src/main/java/com/github/ignaciotcrespo/junitwithparams/WithParamsSource.java @@ -0,0 +1,101 @@ +/** + * The MIT License + *

+ * Copyright (c) 2017, Ignacio Tomas Crespo (itcrespo@gmail.com) + *

+ * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + *

+ * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + *

+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.github.ignaciotcrespo.junitwithparams; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Parameterised test annotation that reads parameter sets from a static method in the + * test class, avoiding the need to embed large value arrays directly on the annotation. + * + *

The provider method must: + *

    + *
  • be {@code static}
  • + *
  • accept no arguments
  • + *
  • return {@code String[][]}, where each inner array represents one test iteration
  • + *
+ * + *

The {@link #names()} attribute maps the column indices of each row to parameter + * names that can be read in the test body. The number of names must equal the length + * of every row returned by the provider. + * + *

Single-parameter example

+ *
+ * {@literal @}Test
+ * {@literal @}WithParamsSource("provideWords")
+ * public void wordLength() {
+ *     String word = params.get();
+ *     assertTrue(word.length() > 0);
+ * }
+ *
+ * static String[][] provideWords() {
+ *     return new String[][] { {"hello"}, {"world"}, {"foo"} };
+ * }
+ * 
+ * + *

Multiple-parameter example

+ *
+ * {@literal @}Test
+ * {@literal @}WithParamsSource(value = "provideNumbers", names = {"n1", "n2", "result"})
+ * public void sum() {
+ *     int n1 = params.asInt("n1");
+ *     int n2 = params.asInt("n2");
+ *     assertEquals(params.asInt("result"), calculator.sum(n1, n2));
+ * }
+ *
+ * static String[][] provideNumbers() {
+ *     return new String[][] {
+ *         {"1",  "2",  "3"},
+ *         {"11", "-2", "9"}
+ *     };
+ * }
+ * 
+ * + * @see WithParamsRule + * @see WithParams + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD}) +public @interface WithParamsSource { + + /** + * Name of the static, no-argument method in the test class that provides the + * parameter sets as a {@code String[][]}. + * + * @return the provider method name + */ + String value(); + + /** + * Parameter names that map to the columns in each row returned by the provider + * method. The number of names must equal the number of elements in every row. + * Defaults to a single name {@code "param1"}. + * + * @return the parameter names + */ + String[] names() default {"param1"}; +} diff --git a/src/test/java/com/github/ignaciotcrespo/junitwithparams/ParameterizedStatementTest.java b/src/test/java/com/github/ignaciotcrespo/junitwithparams/ParameterizedStatementTest.java index 063dd5b..1abfa12 100644 --- a/src/test/java/com/github/ignaciotcrespo/junitwithparams/ParameterizedStatementTest.java +++ b/src/test/java/com/github/ignaciotcrespo/junitwithparams/ParameterizedStatementTest.java @@ -33,7 +33,7 @@ public void setUp() throws Exception { statement = new ParameterizedStatement( Collections.emptySet(), new HashMap(), - null, null, null, usedParams); + null, null, null, usedParams, null); } @Test diff --git a/src/test/java/com/github/ignaciotcrespo/junitwithparams/WithEnumParamsTest.java b/src/test/java/com/github/ignaciotcrespo/junitwithparams/WithEnumParamsTest.java new file mode 100644 index 0000000..87abc30 --- /dev/null +++ b/src/test/java/com/github/ignaciotcrespo/junitwithparams/WithEnumParamsTest.java @@ -0,0 +1,96 @@ +package com.github.ignaciotcrespo.junitwithparams; + +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.Test; + +import java.util.HashSet; +import java.util.Set; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +/** + * Integration tests for {@link WithEnumParams}. + * + *

Verifies that every enum constant is visited exactly once and that + * {@link WithParamsRule#asEnum(Class)} and {@link WithParamsRule#asEnum(String, Class)} + * return the correct typed constant. + */ +public class WithEnumParamsTest { + + enum Color { RED, GREEN, BLUE } + + enum Direction { NORTH, SOUTH, EAST, WEST } + + enum Single { ONLY } + + @Rule + public WithParamsRule params = new WithParamsRule(); + + // --- state for testedForEachEnumConstant (Color, default name) --- + private static Set testedColors; + + // --- state for testedForEachDirectionNamed (Direction, named param) --- + private static Set testedDirections; + + // --- state for singleConstantIteratesOnce --- + private static int singleIterationCount; + + @BeforeClass + public static void beforeClass() { + testedColors = new HashSet(); + testedDirections = new HashSet(); + singleIterationCount = 0; + } + + @Test + @WithEnumParams(Color.class) + public void testedForEachEnumConstant() { + Color color = params.asEnum(Color.class); + + assertNotNull("Enum value should not be null", color); + if (testedColors.contains(color)) fail("Color " + color + " was already tested!"); + testedColors.add(color); + } + + @Test + @WithEnumParams(value = Direction.class, name = "dir") + public void testedForEachDirectionNamed() { + Direction dir = params.asEnum("dir", Direction.class); + + assertNotNull(dir); + if (testedDirections.contains(dir)) fail("Direction " + dir + " was already tested!"); + testedDirections.add(dir); + } + + @Test + @WithEnumParams(Single.class) + public void singleConstantIteratesOnce() { + Single value = params.asEnum(Single.class); + assertEquals(Single.ONLY, value); + singleIterationCount++; + } + + @AfterClass + public static void afterClass() { + // Color assertions + assertEquals("All Color constants should be tested", Color.values().length, testedColors.size()); + assertTrue(testedColors.contains(Color.RED)); + assertTrue(testedColors.contains(Color.GREEN)); + assertTrue(testedColors.contains(Color.BLUE)); + + // Direction assertions + assertEquals("All Direction constants should be tested", Direction.values().length, testedDirections.size()); + assertTrue(testedDirections.contains(Direction.NORTH)); + assertTrue(testedDirections.contains(Direction.SOUTH)); + assertTrue(testedDirections.contains(Direction.EAST)); + assertTrue(testedDirections.contains(Direction.WEST)); + + // Single constant + assertEquals("Single-constant enum should iterate exactly once", 1, singleIterationCount); + } +} diff --git a/src/test/java/com/github/ignaciotcrespo/junitwithparams/WithNullParamTest.java b/src/test/java/com/github/ignaciotcrespo/junitwithparams/WithNullParamTest.java new file mode 100644 index 0000000..1f6d72b --- /dev/null +++ b/src/test/java/com/github/ignaciotcrespo/junitwithparams/WithNullParamTest.java @@ -0,0 +1,89 @@ +package com.github.ignaciotcrespo.junitwithparams; + +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +/** + * Integration tests for {@link WithNullParam}. + * + *

Verifies that the annotation runs the test exactly twice: once with the non-null + * value and once with {@code null}, both for the default parameter name and for a + * custom named parameter. + */ +public class WithNullParamTest { + + @Rule + public WithParamsRule params = new WithParamsRule(); + + // --- state for testedForEachValue --- + private static boolean testedNonNull; + private static boolean testedNull; + + // --- state for testedForEachValueNamed --- + private static boolean testedNonNullNamed; + private static boolean testedNullNamed; + + // --- state for nullIterationReturnsNull --- + private static int nullCount; + + @BeforeClass + public static void beforeClass() { + testedNonNull = false; + testedNull = false; + testedNonNullNamed = false; + testedNullNamed = false; + nullCount = 0; + } + + @Test + @WithNullParam("hello") + public void testedForEachValue() { + String value = params.get(); + + if (testedNonNull && "hello".equals(value)) fail("Non-null was already tested!"); + if (testedNull && value == null) fail("Null was already tested!"); + + if (!testedNonNull && "hello".equals(value)) testedNonNull = true; + if (!testedNull && value == null) testedNull = true; + } + + @Test + @WithNullParam(value = "world", name = "input") + public void testedForEachValueNamed() { + String value = params.get("input"); + + if (testedNonNullNamed && "world".equals(value)) fail("Non-null named was already tested!"); + if (testedNullNamed && value == null) fail("Null named was already tested!"); + + if (!testedNonNullNamed && "world".equals(value)) testedNonNullNamed = true; + if (!testedNullNamed && value == null) testedNullNamed = true; + } + + @Test + @WithNullParam("something") + public void nullIterationReturnsNullValue() { + String value = params.get(); + if (value == null) { + nullCount++; + assertNull(value); + } else { + assertEquals("something", value); + } + } + + @AfterClass + public static void afterClass() { + assertTrue("Non-null value was never tested", testedNonNull); + assertTrue("Null value was never tested", testedNull); + assertTrue("Non-null named value was never tested", testedNonNullNamed); + assertTrue("Null named value was never tested", testedNullNamed); + assertEquals("Expected exactly one null iteration in nullIterationReturnsNullValue", 1, nullCount); + } +} diff --git a/src/test/java/com/github/ignaciotcrespo/junitwithparams/WithParamsRuleTest.java b/src/test/java/com/github/ignaciotcrespo/junitwithparams/WithParamsRuleTest.java index 73371d2..fefc085 100644 --- a/src/test/java/com/github/ignaciotcrespo/junitwithparams/WithParamsRuleTest.java +++ b/src/test/java/com/github/ignaciotcrespo/junitwithparams/WithParamsRuleTest.java @@ -331,4 +331,97 @@ public AtomicInteger to(final String from) { } }; + // ----------------------------------------------------------------------- + // asEnum tests + // ----------------------------------------------------------------------- + + private enum TestColor { RED, GREEN, BLUE } + + @Test + public void asEnum_default() throws Exception { + rule.paramsMap = new HashMap(); + rule.paramsMap.put(WithParamsRule.PARAM_DEFAULT, "RED"); + + TestColor value = rule.asEnum(TestColor.class); + + assertThat(value).isEqualTo(TestColor.RED); + } + + @Test + public void asEnum_name() throws Exception { + rule.paramsMap = new HashMap(); + rule.paramsMap.put("color", "GREEN"); + + TestColor value = rule.asEnum("color", TestColor.class); + + assertThat(value).isEqualTo(TestColor.GREEN); + } + + @Test + public void asEnum_allConstants() throws Exception { + for (TestColor expected : TestColor.values()) { + rule.paramsMap = new HashMap(); + rule.paramsMap.put(WithParamsRule.PARAM_DEFAULT, expected.name()); + + TestColor value = rule.asEnum(TestColor.class); + + assertThat(value).isEqualTo(expected); + } + } + + @Test + public void asEnum_nullValue_returnsNull() throws Exception { + rule.paramsMap = new HashMap(); + rule.paramsMap.put(WithParamsRule.PARAM_DEFAULT, null); + + TestColor value = rule.asEnum(TestColor.class); + + assertThat(value).isNull(); + } + + @Test + public void asEnum_name_nullValue_returnsNull() throws Exception { + rule.paramsMap = new HashMap(); + rule.paramsMap.put("color", null); + + TestColor value = rule.asEnum("color", TestColor.class); + + assertThat(value).isNull(); + } + + @Test + public void asEnum_invalidValue_throwsIllegalArgument() throws Exception { + thrown.expect(IllegalArgumentException.class); + rule.paramsMap = new HashMap(); + rule.paramsMap.put(WithParamsRule.PARAM_DEFAULT, "INVALID_COLOR"); + + rule.asEnum(TestColor.class); + } + + @Test + public void asEnum_name_invalidValue_throwsIllegalArgument() throws Exception { + thrown.expect(IllegalArgumentException.class); + rule.paramsMap = new HashMap(); + rule.paramsMap.put("color", "NOT_A_COLOR"); + + rule.asEnum("color", TestColor.class); + } + + @Test + public void asEnum_missingKey_throwsRuntimeException() throws Exception { + thrown.expect(RuntimeException.class); + rule.paramsMap = new HashMap(); + + rule.asEnum(TestColor.class); + } + + @Test + public void asEnum_name_missingKey_throwsRuntimeException() throws Exception { + thrown.expect(RuntimeException.class); + rule.paramsMap = new HashMap(); + rule.paramsMap.put("other", "RED"); + + rule.asEnum("color", TestColor.class); + } + } \ No newline at end of file diff --git a/src/test/java/com/github/ignaciotcrespo/junitwithparams/WithParamsSourceTest.java b/src/test/java/com/github/ignaciotcrespo/junitwithparams/WithParamsSourceTest.java new file mode 100644 index 0000000..ad616c1 --- /dev/null +++ b/src/test/java/com/github/ignaciotcrespo/junitwithparams/WithParamsSourceTest.java @@ -0,0 +1,135 @@ +package com.github.ignaciotcrespo.junitwithparams; + +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * Integration tests for {@link WithParamsSource}. + * + *

Covers: + *

    + *
  • Multi-parameter source method with named params ({@link #sum()})
  • + *
  • Single-parameter source method with default param name ({@link #wordIsNotEmpty()})
  • + *
  • Empty source method producing zero iterations ({@link #neverRunsForEmpty()})
  • + *
  • Null-returning source method producing zero iterations ({@link #neverRunsForNull()})
  • + *
+ */ +public class WithParamsSourceTest { + + @Rule + public WithParamsRule params = new WithParamsRule(); + + private static List sumResults; + private static List words; + private static int emptyIterations; + private static int nullIterations; + + @BeforeClass + public static void beforeClass() { + sumResults = new ArrayList(); + words = new ArrayList(); + emptyIterations = 0; + nullIterations = 0; + } + + // ----------------------------------------------------------------------- + // Multi-parameter: named params + // ----------------------------------------------------------------------- + + @Test + @WithParamsSource(value = "provideSums", names = {"n1", "n2", "result"}) + public void sum() { + int n1 = params.asInt("n1"); + int n2 = params.asInt("n2"); + int expected = params.asInt("result"); + assertEquals(expected, n1 + n2); + sumResults.add(expected); + } + + static String[][] provideSums() { + return new String[][]{ + {"1", "2", "3"}, + {"11", "-2", "9"}, + {"0", "0", "0"} + }; + } + + // ----------------------------------------------------------------------- + // Single-parameter: default param name + // ----------------------------------------------------------------------- + + @Test + @WithParamsSource("provideWords") + public void wordIsNotEmpty() { + String word = params.get(); + assertTrue("Word should not be empty", word != null && !word.isEmpty()); + words.add(word); + } + + static String[][] provideWords() { + return new String[][]{ + {"hello"}, + {"world"}, + {"foo"} + }; + } + + // ----------------------------------------------------------------------- + // Empty source — zero iterations + // ----------------------------------------------------------------------- + + @Test + @WithParamsSource("provideEmpty") + public void neverRunsForEmpty() { + emptyIterations++; + } + + static String[][] provideEmpty() { + return new String[0][]; + } + + // ----------------------------------------------------------------------- + // Null source — zero iterations + // ----------------------------------------------------------------------- + + @Test + @WithParamsSource("provideNull") + public void neverRunsForNull() { + nullIterations++; + } + + static String[][] provideNull() { + return null; + } + + // ----------------------------------------------------------------------- + // @AfterClass verification + // ----------------------------------------------------------------------- + + @AfterClass + public static void afterClass() { + // sum + assertEquals("Expected 3 sum iterations", 3, sumResults.size()); + assertTrue(sumResults.contains(3)); + assertTrue(sumResults.contains(9)); + assertTrue(sumResults.contains(0)); + + // words + assertEquals("Expected 3 word iterations", 3, words.size()); + assertTrue(words.contains("hello")); + assertTrue(words.contains("world")); + assertTrue(words.contains("foo")); + + // empty / null source + assertEquals("Empty source should produce zero iterations", 0, emptyIterations); + assertEquals("Null source should produce zero iterations", 0, nullIterations); + } +}