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)}. + * + *
+ * enum Color { RED, GREEN, BLUE }
+ *
+ * {@literal @}Test
+ * {@literal @}WithEnumParams(Color.class)
+ * public void testAllColors() {
+ * Color color = params.asEnum(Color.class);
+ * assertNotNull(color);
+ * }
+ *
+ *
+ *
+ * {@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 extends Enum>> 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. + * + *
+ * {@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
+ * }
+ *
+ *
+ *
+ * {@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.).
+ *
+ *
+ * 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 Returns {@code null} if {@code null} was injected (e.g. via {@link WithNullParam}).
+ *
+ * @param Returns {@code null} if {@code null} was injected (e.g. via {@link WithNullParam}).
+ *
+ * @param
+ * 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:
+ * 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.
+ *
+ * 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 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 Covers:
+ *
+ *
+ *
+ * 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.
+ *
+ */
+public class WithParamsSourceTest {
+
+ @Rule
+ public WithParamsRule params = new WithParamsRule();
+
+ private static List