New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support JUnit 5 #11
Comments
Hi Jonathan, At Google, we almost exclusively use JUnit4, which is why we haven't felt this need ourselves yet. But we did anticipate this being requested once we open source the project. Looks like you are the first one requesting this :-). I'll look into how to do this when I get some time. If anyone has pointers on how this is best done, that would be appreciated :-). |
@nymanjens Thanks for acknowledging this so quickly! Re. pointers, JUnit 5 provides individual extension points for various parts of the test lifecycle, rather than a super-flexible test runner or rule like JUnit 4 does. There is an official User Guide with a comprehensive reference on the various APIs. But I've found it more helpful to use baeldung's guide first, and then read the User Guide and javadocs or study an existing extension like those in junit-pioneer to get a better feel on what can be done and how to do things. I hope this helps! |
For those of you looking for a library like TestParameterInjector for JUnit 5 in the meantime, consider using JUnit 5's parameterized tests, junit-pioneer's |
I've investigated this a bit. My ideal scenario would be to have something like this: // Old code
@RunWith(TestParameterInjector.class)
public class MyTest {
@TestParameter boolean isDryRun;
@Test public void test1(@TestParameter boolean enableFlag) {
// ...
}
}
// New code
@ExtendWith(TestParameterInjector.class)
public class MyTest {
@TestParameter boolean isDryRun;
@Test public void test1(@TestParameter boolean enableFlag) {
// ...
}
} i.e. replacing It looks like that's not possible because the only extension interface I could find that allows multiple runs is So the next best API I can think of would look like this: @ExtendWith(TestParameterInjector.class)
public class MyTest {
@TestParameter boolean isDryRun;
@TestParameterInjector.Test
public void test1(@TestParameter boolean enableFlag) {
// ...
}
@TestParameterInjector.Test
public void test2() {
// ...
}
} |
@nymanjens Ah, that's a pain. Alternatively you may be able to make a custom "source" for public class MyTest {
@TestParameter boolean isDryRun;
@ParameterizedTest
@TestParameterInjectorSource
public void test1(@TestParameter boolean enableFlag) {
// ...
}
@ParameterizedTest
@TestParameterInjectorSource
public void test2() {
// ...
}
} On its own, this wouldn't any better. But you may be able to create a new annotation like public class MyTest {
@TestParameter boolean isDryRun;
@TestParameterInjector.Test
public void test1(@TestParameter boolean enableFlag) {
// ...
}
@TestParameterInjector.Test
public void test2() {
// ...
}
} ...which looks more similar to the JUnit 4-based API to me. |
The JUnit 5 maintainers will have more insight on this than me, so try raising an issue on the JUnit 5 issue tracker to see if using |
Good point. Actually, by adding So this is the new proposed API then:
|
@nymanjens Actually, it looks like we might be able to make this even more concise. In the next release of JUnit 5 - 5.8 - So the following annotation will be possible: @Target({ ElementType.FIELD, ElementType.PARAMETER })
@Retention(RetentionPolicy.RUNTIME)
@ExtendWith(TestParameterInjectorExtension.class)
public @interface TestParameter {
} And in turn your example API usage can be shortened to: public class MyTest {
@TestParameter boolean isDryRun;
@Test
public void test1(@TestParameter boolean enableFlag) {
// ...
}
@Test
public void test2() {
// ...
}
} This makes it just like TestParameterInjector's JUnit 4-based API, minus a |
Cool, thanks for the info. That sounds ideal :-D |
https://junit.org/junit5/docs/snapshot/release-notes/#release-notes-5.8.0 5.8 has landed the feature necessary, is there ongoing work? |
This is on my TODO list, but I haven't gotten around to this yet. |
Also, thanks for letting me know about the feature landing :-) |
I've investigated this some more. My conclusions:
So given the constraints above, this is the new proposed API: @ExtendWith(TestParameterInjectorExtension.class)
class MyTest {
@TestParameter boolean field;
@TestParameterInjectorTest
void myTest() { ... }
@TestParameterInjectorTest
@TestParameters2({"{a: 1}", "{a: 2}"})
void withParameters_success(int a) { ... }
@TestParameterInjectorTest
void withParameter_success(@TestParameter boolean b, @TestParameter boolean c) { ... }
} |
Nice investigation! Mind you that "extendswith" is additive and there could be other
(Quick note: TPIT is pretty a mouthful, imagine every developer having to write that every time they write a test.) |
re JUnit 4 v 5, I totally agree testing helpers extending JUnit rules is a plague when it comes to pure JUnit 5 tests. |
Yep, that's the plan. I'm currently refactoring the implementation to make it JUnit4-agnostic. The public types
I agree it's a mouthful, and I'm open to alternative suggestions. I've considered |
Yeah, not a fan of abbreviations either, I had some thinking and couldn't come up with any better name 😢. Just found a new thing, regarding |
As I understand it, if you don't add Method parameterization is probably the most commonly used one, but field parameterization is definitely occasionally useful, and used enough to be confusing if it weren't supported. It could work by only requiring |
I had a play and this is what I learned:
annotation class TP
@ExtendWith(TPIE::class)
@Test
annotation class TPIT TestParameterInjectorExtension implimport org.junit.jupiter.api.extension.BeforeEachCallback
import org.junit.jupiter.api.extension.ExtensionContext
import org.junit.jupiter.api.extension.ParameterContext
import org.junit.jupiter.api.extension.ParameterResolver
import org.junit.jupiter.api.extension.TestInstanceFactory
import org.junit.jupiter.api.extension.TestInstanceFactoryContext
import org.junit.jupiter.api.extension.TestInstancePostProcessor
class TPIE : BeforeEachCallback, TestInstancePostProcessor, ParameterResolver/*, TestInstanceFactory*/ {
// override fun createTestInstance(factoryContext: TestInstanceFactoryContext, extensionContext: ExtensionContext): Any? {
// val ctor = factoryContext.testClass.constructors.single()
// val outer = factoryContext.outerInstance.map { listOf(it) }.orElse(emptyList())
// val args = ctor.parameters.drop(outer.size).map { "ctor arg from ext for ${it.name}" }
// return ctor.newInstance(*(outer + args).toTypedArray())
// }
override fun postProcessTestInstance(testInstance: Any, context: ExtensionContext) {
println("postProcessTestInstance ${testInstance}")
testInstance::class.java.declaredFields
.filter { it.isAnnotationPresent(TP::class.java) }
.forEach { it.set(testInstance, "field from ext") }
}
override fun beforeEach(context: ExtensionContext) {
println("beforeEach ${context.displayName}")
}
override fun supportsParameter(parameterContext: ParameterContext, extensionContext: ExtensionContext): Boolean =
parameterContext.isAnnotated(TP::class.java)
override fun resolveParameter(parameterContext: ParameterContext, extensionContext: ExtensionContext): Any =
"param from ext"
} Test examples(ignore the import org.junit.jupiter.api.Nested
class TPITTests {
@Nested inner class OnParam {
@TPIT fun test(@TP param: String) {
println(param) // param from ext
}
}
@Nested inner class OnField {
@field:TP lateinit var field: String
@TPIT fun test() {
println(field) // field from ext
}
}
@Nested inner class OnCtor constructor(
@param:TP private val ctor: String
) {
@TPIT fun test() {
println(ctor) // param from ext
}
}
@Nested inner class Combo constructor(
@param:TP private val ctor: String
) {
@field:TP lateinit var field: String
@TPIT fun test(@TP param: String) {
println("ctor=$ctor field=$field param=$param") // ctor=param from ext field=field from ext param=param from ext
}
}
@Nested inner class Mixed {
@field:TP lateinit var field: String
@TPIT fun test(@TP param: String) {
println("field=$field param=$param") // field=field from ext param=param from ext
}
@Test fun normal() {
println(::field.isInitialized) // false
}
@Test fun mismatch(@TP param: String) {
// error: No ParameterResolver registered for parameter
}
}
} |
Excellent investigations, both! It's a bit gutting to see that we cannot use And hopefully there's a way we can avoid forcing users to use |
As for acronyms for import com.google.testing.junit.jupiter.testparameterinjector.TestParameterInjector
import com.google.testing.junit.jupiter.testparameterinjector.TestParameterInjector.Test
...
@TestParameterInjector
class MyTest {
@TestParameter boolean field;
@Test
void myTest() { ... }
@Test
@TestParameters2({"{a: 1}", "{a: 2}"})
void withParameters_success(int a) { ... }
@Test
void withParameter_success(@TestParameter boolean b, @TestParameter boolean c) { ... }
} A disadvantage is users could easily get |
Careful, remember that code is read more than written. It can be easily confused, agreed; also you would get into problems with automatic formatters (forcing qualified usages), or wanting to use parameterized and non-paramterized tests in the same class. The same applies for a meta-annotated extension annotation. Does it actually bring benefits? Having 4 ExtendsWith on the class is cleaner than 4 random annotations that might or might not extend. |
I think the But I agree with everything else you've said. :) |
Might be an unrelated conversation here, @nymanjens feel free to collapse this. At https://github.com/junit-pioneer/junit-pioneer/pull/491/files#diff-7b1c59658c736ae3c2131a2425f2f8c88f264a605dd0c0a14dee256d665860c9R18-R21 @jbduncan is double-indirecting the @ExtendsWith(SomethingExtension::class)
annotation class Something for the sake of it, so that you can write @Something
class MyTest unless it adds extra value, for example parameters, or more meta-annotations; before then, it's yagni. Note that TPIT is there to stay (whatever its name) because of the templating constraints (see point 2 in #11 (comment)) |
@TWiStErRob Good argument re. my meta-annotation idea not adding anything extra behaviour-wise, I'm convinced! |
@TWiStErRob Thanks for your investigation! I confirm your conclusions: The
Proposed API
Questions I'm still investigating:
TestParameterInjectorExtension high level implementation
|
…d processors JUnit4 independent See #11.
…erProcessor (Google-internal feature) See #11.
…combining abstraction of all processors. See #11.
… that don't depend on JUnit4. This is a prerequisite for #11.
I've just pushed a commit tha adds basic support for JUnit5 in new Maven target. Notable things that are missing in this first version:
|
I quite like the look of this API (well done, @nymanjens!). But I'm a JUnit 5 user, so if I wanted to use this library, I'd have to port it to a JUnit 5 extension, or use a potentially more verbose alternative.
Is this something that's been considered already, by any chance?
The text was updated successfully, but these errors were encountered: