I'm trying to write UT for my Gradle plugin. I faced a wall while trying to mock Gradle Project methods. Under the hood, I'm using Gradle Properties to calculate the plugin's extension providers. Example:
this.outputDir = project.getObjects().property(String.class);
this.outputDir.convention(project.provider(() -> {
if (project.hasProperty("com.example.outputDir")) {
return project.property("com.example.outputDir").toString();
}
return "./default-output";
}));
I wanted to write simple tests checking if I provide the com.example.outputDir property, it will be returned by the convention. So I figured out, that I will mock project.hasProperty and project.property:
// Creating Gradle Project on the fly
ProjectBuilder builder = ProjectBuilder.builder();
// Mocking the Project (Project is an interface)
Project testProject = Mockito.spy(builder.build());
// Applying the plugin to the mocked project
testProject.getPlugins().apply(PLUGIN_NAME);
// Enabling mock
when(testProject.hasProperty("com.example.outputDir")).thenReturn(true);
// This works (obviously)
Assertions.assertEquals(true, testProject.hasProperty("com.example.outputDir"));
// This does not work
MyExtension ext = (MyExtension) testProject.getExtensions().getByName("my-extension");
Assertions.assertEquals("hello-world", ext.ouputDir.get());
After digging in into IntelliJ debugger, it seems that Gradle is keeping a registry of the projects... So when the extension is created, it's getting a reference to the original project, not mocked one. Which is super weird for me, because I'm using testProject.getPlugins().apply(PLUGIN_NAME); so why on earth Gradle would not use a reference of the project that has called the function, and instead it's checking "own source"?
So I'm wondering if is it even possible to test and mock Gradle Projects (and Plugins)? I heard that Gradle is a super popular tool, but there is zero articles/documentation on how to properly test and mock Gradle Plugins.
I would appreciate for any help and guidance in this area.
Related
I am writing a custom Gradle plugin which shall unifiedly abstract from my custom gradle configurations which are spread across multiple Java projects. For this purpose, I generally try to find fitting extensions for the various tasks that need custom configuration.
So, I got to the point where I wanted to move my JaCoCo configuration to the plugin. The excerpt from build.gradle looks like this:
jacocoTestReport {
reports {
xml.required = true
}
}
The first part is manageable: check if the plugin is loaded.
project.getPlugins().withType(JacocoPlugin.class, jacocoPlugin -> {
// configure it
})
However, I am stuck with how to actually configure the plugin via an extension method. The only extension that is available seems to be JacocoPluginExtension. From there, I don't see a way how to add the reports part from build.gradle.
Is there some other mechanism besides extensions that I missed?
'jacocoTestReport' is actually a task, so you can configure it like this:
proj.tasks.getByName("jacocoTestReport", {
reports {
xml.required = true
}
})
If you'd like to dig deeper, it's class is JacocoReport, which has a
JacocoReportsContainer reports property, so you may use type safe property getters / setters from there, but IMHO the above solution is more like build.gradle, so it's more readable.
I'm removing Powermock from the project I'm currently working on, so I'm trying to rewrite some existing unitary test only with Mockito (mockito-core-2.2.28).
When I run the test, I have the following error:
org.mockito.exceptions.base.MockitoException:
Cannot mock/spy class com.ExternalpackagePath.Externalclass
Mockito cannot mock/spy because :
final class
I know that this question has already been asked (How to mock a final class with mockito, Mock objects calling final classes static methods with Mockito), but I didn't find the answer I'm looking for.
Here is an extract of my code :
public class MyClassToTest extends TestCase {
private MyClass myClass;
#Mock private Externalclass ext; // This class is final, I would like to mock it
#Override
protected void setUp() throws Exception {
MockitoAnnotations.initMocks(this); // <<<< The exception is thrown here
ext = Mockito.mock(Externalclass.class);
}
}
As mentioned in the Mockito documentation (https://github.com/mockito/mockito/wiki/What%27s-new-in-Mockito-2, §Mock the unmockable), I added the org.mockito.plugins.MockMaker file. This is the tree of my project :
project
src
com.packagePath.myPackage
myClass
test
com.packagePath.myPackage
myClassToTest
resources
mockito-extensions
org.mockito.plugins.MockMaker
I also tries to put the "resources" directory in "src", in a subdir called "test", but the result is still the same.
I thought that mocking a final was possible with Mockito v2. Does someone have an idea of what is missing here ?
Thanks!
Weird that your solution seems to work.
According to their documentation on Github it says.
Mocking of final classes and methods is an incubating, opt-in feature. It uses a combination of Java agent instrumentation and subclassing in order to enable mockability of these types. As this works differently to our current mechanism and this one has different limitations and as we want to gather experience and user feedback, this feature had to be explicitly activated to be available ; it can be done via the mockito extension mechanism by creating the file src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker containing a single line:
mock-maker-inline
After you created this file, Mockito will automatically use this new engine and one can do :
final class FinalClass {
final String finalMethod() { return "something"; }
}
FinalClass concrete = new FinalClass();
FinalClass mock = mock(FinalClass.class);
given(mock.finalMethod()).willReturn("not anymore");
assertThat(mock.finalMethod()).isNotEqualTo(concrete.finalMethod());
In subsequent milestones, the team will bring a programmatic way of using this feature. We will identify and provide support for all unmockable scenarios. Stay tuned and please let us know what you think of this feature!
My working structure now looks like this.
I couldn't get it working with the configuration file either; however, the Mockito team is so kind and also provides a pre-configured Mockito artifact that requires no configuration in the target project.
As a convenience, the Mockito team provides an artifact where this mock maker is preconfigured. Instead of using the mockito-core artifact, include the mockito-inline artifact in your project. Note that this artifact is likely to be discontinued once mocking of final classes and methods gets integrated into the default mock maker.
So, if you use Gradle and want to test your Kotlin code, just add this to your project's dependencies:
testCompile 'org.mockito:mockito-inline:2.8.9'
testCompile('com.nhaarman:mockito-kotlin:1.5.0') {
exclude group: 'org.jetbrains.kotlin'
exclude group: 'org.mockito'
}
Well, I found what's wrong here, it maybe useful for other people. My project tree is wrong, I put the org.mockito.plugins.MockMaker in a directory "mockito-extension" directly in "src". This is my tree now:
projet
src
com.packagePath.myPackage
myClass
mockito-extensions
org.mockito.plugins.MockMaker
test
com.packagePath.myPackage
myClassToTest
You seem to have had a classpath issue, just like I did.
Your previous setup would have also worked, but it seems like
project/test/resources
was not in your classpath.
I had the same issue when I tried to run this with IntelliJ. I simply marked the resources directory as a Test Resources Root and it worked fine. Praise the gods of Mockito!
I had the same issue that you described. For me, the solution was to create a file named org.mockito.plugins.MockMaker in /test/java/resources/mockito-extensions/ directory and write the following line: mock-maker-inline.
So MockMaker is actually the file extension (no txt, properties or any other extension needed).
I also encountered the same issue.
This worked for me: How to use the Mockito's inline mock maker: Option 2
Before Mockito can be used for mocking final classes and methods, it needs to be configured. Based on your screenshot of your project tree, it seems that the location of MockMaker file is incorrect.
Create (if the file still does not exist) or update MockMaker file in the path below
src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
... and add this line mock-maker-inline.
If you have multiple modules in project check out if they also have some references to Mockito. For me the problem was deprecated and unnecessary definition in some other small and forgotten library module:
testCompile 'org.mockito:mockito-all:1.10.19'
Removing this unnecessary declaration solved the problem for me
After following configuration instruction, I still wasn't able to get it working.
For me it was due to JDK I was using. After switching to a different one (different provider) the solution with org.mockito.plugins.MockMaker file worked.
I went from JDK with Hotswap agent (trava-jdk-11-dcevm / dcevm-11.0.11+1) to Eclipse adoptOpenJDK (temurin-11.0.14).
This solution worked for me:
Instead of
testCompile "org.mockito:mockito-android:2.9.0"
in the gradle file, replace it with
testCompile group: 'org.mockito', name: 'mockito-inline', version: '2.9.0'
and it would work.
I'm using gradle to build my android project and am not able to run single local unit test. I have several test classes and one of them is MockServerTest and I only want to run test methods in this class.
I tried using gradle -Dtest.single=MockServerTest test but it turned out running all my tests, including these in other test classes.
I also tried gradle test --tests MockServerTest but an error occurred said
Test filtering is not supported for given version of junit. Please upgrade junit version to at least 4.6.
But I'm using junit 4.12 in my gradle file
testCompile 'junit:junit:4.12'
I'm using gradle 2.4 with com.android.tools.build:gradle:1.2.3.
Also, how can I run a single test method inside a single test class?
BTW, I'm able to run single test method inside Android Studio, by right clicking on the test method and select run targetTestMethod() from the menu. But how can I achieve this in the terminal? I guess Android Studio also trigger a certain command to do this. How can I see what that command is?
Figured it out myself. I have to run
gradle testDebug --tests com.my.package.TestClassName
There are two things to note here.
1. You have to use gradle testDebug or gradle testRelease instead of just gradle test. If you have build variant, you have to use gradle testVariantNameDebug or gradle testVariantNameRelease
2. You have to specify the whole qualified class name, means including the package name.
You can use Android Gradle plugin DSL to set up test tasks filters like this:
android {
testOptions {
unitTests.all {
it.testNameIncludePattern = "*.SomeTest"
}
}
}
You can find more information on testOptions here and filters here.
Have you tried running gradle test -Dtest.single=MockServerTest? More information can be found here.
I'm the author of one of the Maven plugins (not Apache/Codehaus, completely indie). Sometimes I get support requests or test cases where I'd really need to debug the execution of my plugin with an existing pom.xml. Basically the test cases I get are sample/test project (pom.xml with src/main/resoures, src/main/java and so on).
What I need is a way to:
Load an existing pom.xml.
Find a specific execution of my plugin there (usually it's the only one).
Get an instance of MyMojo - fully initialized/condigured, with all the components and parameters corectly injected.
Execute MyMojo.
What's important is that test projects are separate projects, I don't want to copy them into the Maven module of my plugin.
I'd like to be able to do this without remote debugging.
By debugging I mean to be able to set and halt on breakpoints (also conditional), step in/out/over on the source code.
Ideally I'd like to be able to executeMyMojoFrom(new File("pom.xml")) - for instance in a JUnit test or a main method of some class. (I can supply groupId, artifactId etc. All other definitions should just be loaded from that pom.xml.)
How can I achieve this?
What I've tried so far:
Debug As... on pom.xml in Eclipse - does not work well enough (source code not found, breakpoint don't work as its not a Java project context)
Maven Embedder/Invoker solutions - spawn things in separate processes via CLI. Forget breakpoints, no debugging.
Remote debugging with mvnDebug and then remote debugging from Eclipse as suggested by Pascal Thivent here. This is so far the best option. However, remote debugging means starting mvnDebug separately, and there's also not guarantee that the JARs I have in Eclipse are exactly the same that mvnDebug is using. So there's a certain distance here.
maven-plugin-testing-harness - I actually thought this this will do the task. But first I was jumping through hoops for a few hours just to make it start. All of the important dependencies are "provided" so I first had to figure out the right combination of versions of these artifacts. And then - only to discover that AbstractMojoTestCase only works within the plugin module you want to test. Probably I was mistaken when I thought that maven-plugin-testing-harness was a testing harness for Maven plugins. It seems that it's a testing harness for the plugin from that plugin's module. Which is not illogical but does not help my case. I'd like to test my plugin in other modules.
So right now I've got the best results with the remote debugging solution. But what I'm looking for is really something like maven-plugin-testing-harness but not hardwired to the plugin module. Does anyone happen to have a hint, if such a method exists somewhere in Maven artifacts?
To be even more specific, I'd like to write something like:
public void testSomething()
throws Exception
{
File pom = getTestFile( "pom.xml" );
assertNotNull( pom );
assertTrue( pom.exists() );
MyMojo myMojo = (MyMojo) lookupMojo( "myGroupId", "myArtifactid", ...,
"myGoal", pom );
assertNotNull( myMojo );
myMojo.execute();
...
}
Compare it to the MyMojoTest here - it's almost there. Should just not be hardwired into the mymojo Maven module (as it is in maven-plugin-testing-harness).
Update
Few answers to the questions in comments:
You mean you don't want such a test class, i.e MyMojoTest to reside inside the same project as the MyMojo, i.e your plugin project? Why is that?
Exactly. I want to debug the plugin execution in an existing Maven project, I don't want to move that project to my plugin project first to be able to run a test. I want to be able to test/debug an existing project. Ideally, I'd just need to add my-maven-plugin-testing dependency and subclass MyMojoTest in the project's src/test/jaca. This would be a good instrument to debug executions. Dragging the target project into my Mojo project ist just too much overhead - and mostly these aren't really the test cases I want to keep long-term. I hope, this answers, why.
Anyway, it's merely a convention to keep the project-to-test/pom.xml inside the src/test/resources of your plugin module, not a rule...
My problem is not the location of the pom.xml of the project-to-test, that is easily configurable. My difficulty is that maven-plugin-testing-harness is is somehow hardcoded to be in the Mojo's project. It uses the pom.xml of the Mojo, looks for other special files/descriptors in the containing project. So I somehow can't use it in a non-Mojo project, or can I? This is my question.
And I'm not sure why Debug as... didn't help you...
Not sure either, but (1) breakpoints did not work and (2) the source code was not "attached" for some reason.
If the Debug as didn't work for you as well as it should, you can try to use the mojo-executor with a bit of work.
https://github.com/TimMoore/mojo-executor
This is how you would execute the copy-dependencies goal of the Maven Dependency Plugin programmatically:
executeMojo(
plugin(
groupId("org.apache.maven.plugins"),
artifactId("maven-dependency-plugin"),
version("2.0")
),
goal("copy-dependencies"),
configuration(
element(name("outputDirectory"), "${project.build.directory}/foo")
),
executionEnvironment(
mavenProject,
mavenSession,
pluginManager
)
);
The project, session, and pluginManager variables should be injected via the normal Mojo injection. Yes, that means this should be executed from the context of another maven plugin. Now that I think about it, whether this would help you in any way is still a question because this still relies on injection of such components by the underlying plexus container.
My original idea was though to have you build a maven plugin that would invoke your jaxb2 plugin thru the mojo-executor like above, then serialize the mavenProject, mavenSession, pluginManager, i.e, all the plexus injected components and then use those objects to invoke your jaxb2 plugin in future from a standalone class without the plugin that you built.
I have two related questions here.
In Play 2.2.x, the distribution was bundled as a zip file, and available for download through the maven repository http://downloads.typesafe.com/play/2.2.x/play-2.2.x.zip. This meant that you could use a pom.xml and embed play into your app without needing to use sbt. Given 2.3.x has shifted to the activator model, is it still possible to use it with maven?
And secondly, is it possible to use play 2.3.x without activator at all? (I know they have a sbt plugin for play, but that seems very complex as well).
Thanks!
Activator is only needed to create the empty template project, which you could also do by hand if you know a bit about play. After that empty project is created all you need is sbt (which actually is a pretty central part of activator).
With play 2.3 the distribution model changed from the one big zip-file to regular ivy/maven dependencies, so you could possibly get all dependencies right from a maven project. The problem is that the sbt play setup does so much more: template compilation, routes DSL compilation, hot reloading, asset pipeline stuff, so I don't think maven actually is an option.
Yes.
Example on Github
package io.github.alancnet
import java.io.File
import play.api.{Environment, ApplicationLoader}
object PlayTest {
class Dummy{}
def main(args:Array[String]):Unit = {
def startWebServer = {
val environment = new Environment(
new File("."),
classOf[Dummy].getClassLoader,
play.api.Mode.Dev
)
val context = play.api.ApplicationLoader.createContext(environment)
val application = ApplicationLoader(context).load(context)
play.api.Play.start(application)
play.core.server.NettyServer.fromApplication(
application
)
}
startWebServer
}
}