Skip to content
This repository was archived by the owner on Mar 11, 2022. It is now read-only.

Junit 5 #422

Merged
merged 17 commits into from
Mar 28, 2018
Merged

Junit 5 #422

merged 17 commits into from
Mar 28, 2018

Conversation

tomblench
Copy link
Contributor

@tomblench tomblench commented Feb 28, 2018

What

Upgrade tests to JUnit 5 (JUnit Jupiter)

How

  • Refactor tests to use JUnit 5 (org.junit.jupiter.api.*) APIs

  • Convert existing Resources/Rules etc to JUnit 5 Extensions

  • Add base classes in com.cloudant.tests.base package for common test
    patterns eg remote-data-base-per-class, remote-database-per-method

  • Convert parameterised tests to TestTemplates which use helper
    classes to provide parameters

  • Convert JUnit 4 Categories to JUnit 5 Tags

  • Upgrade gradle to 4.6

  • Add custom JavaExec task to build.gradle to invoke JUnit Platform
    Console Launcher, due to lack of flexibility in current gradle
    support for JUnit Platform.

Testing

All tests pass, with current exceptions:

  • DesignDocumentTest - number of tests run is exactly half on this branch. It appears that the existing code ran every test twice, maybe due to the use of @RunWith(Enclosed.class)
  • HttpProxyTest - two test combinations currently disabled due to failure. Previously it was determined that all test combinations would pass if run in some orderings, but now these two parameter combinations always fail. Needs further investigation.

TODO

  • Remove if (env.BRANCH_NAME == "master" || env.BRANCH_NAME == "junit5") from Jenkinsfile before merging.
  • Re-visit java version requirements. 1.8 is required to build and run tests now, although production code should still only require 1.6 Fixed in 62e82ad
  • Update copyrights
  • Format source files which have been automatically updated to switch order of assert arguments, which now have long lines

* Refactor tests to use JUnit 5 (org.junit.jupiter.api.*) APIs

* Convert existing Resources/Rules etc to JUnit 5 Extensions

* Add base classes in com.cloudant.tests.base package for common test
  patterns eg remote-data-base-per-class, remote-dat abase-per-method

* Convert parameterised tests to TestTemplates which use helper
  classes to provide parameters

* Convert JUnit 4 Categories to JUnit 5 Tags

* Upgrade gradle to 4.6

* Add custom JavaExec task to build.gradle to invoke JUnit Platform
  Console Launcher, due to lack of flexibility in current gradle
  support for JUnit Platform.
@ricellis
Copy link
Member

ricellis commented Mar 9, 2018

Regarding DesignDocumentTest running locally produces one instance each, but I did find this about running at the package level running things twice with Enclosed https://bugs.eclipse.org/bugs/show_bug.cgi?id=474636
Entirely possible this is the same weirdness.

Copy link
Member

@ricellis ricellis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Submitting this first part for now, I'm only about 20% of the way through though, so expect more to follow.

build.gradle Outdated
@@ -28,8 +28,8 @@ subprojects {
// If the version says "snapshot" anywhere assume it is not a release
ext.isReleaseVersion = !version.toUpperCase(Locale.ENGLISH).contains("SNAPSHOT")

sourceCompatibility = 1.6
targetCompatibility = 1.6
sourceCompatibility = 1.8
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it possible to set this property only for the java test compile?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ricellis good idea, see 62e82ad


import java.util.Collections;
import java.util.List;


// This really requires Couch2.0 + text index support, but we don't have a way of expressing that
@Category(RequiresCloudant.class)
@RequiresCloudant
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought one of the objectives of using JUnit 5 tags was being able to separate out different API level tests.
Then we can choose appropriate sets in the matrix for Couch 1.6 and Couch 2 as well as Cloudant.
For example this could be:

@Couch2
@Cloudant

since it applies to any Cloudant or Couch2

Is that intended for a later PR?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably best done in a follow-on PR. I wanted to get the tags exactly the same so that I could check each of the matrix builds executes the correct number of tests, before thinking about changing the tags.


private Database db;
private static Database db;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this static if it is a @BeforeEach?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tomblench think you missed this one

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in fe2ecb7 by extending TestWithDbPerTest

@Tag("RequiresCloudant")
@Tag("RequiresDB")
public @interface RequiresCloudant {
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

newline

@Retention(RetentionPolicy.RUNTIME)
@Tag("RequiresCloudant")
@Tag("RequiresDB")
public @interface RequiresCloudant {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you feel this level of indirection is preferable to tagging the tests directly? I guess it made replacing the categories easier :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These tags were straight out of the JUnit guide, so it's probably considered best practice.

@Tag("RequiresCloudant")
@Tag("RequiresDB")
public @interface RequiresCloudantService {
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

newline

@Tag("RequiresCouch")
@Tag("RequiresDB")
public @interface RequiresCouch {
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

newline

@Retention(RetentionPolicy.RUNTIME)
@Tag("RequiresDB")
public @interface RequiresDB {
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

newline

byte[] bytesToDB = "binary data".getBytes();
ByteArrayInputStream bytesIn = new ByteArrayInputStream(bytesToDB);
Response response = db.saveAttachment(bytesIn, "foo.txt", "text/plain", null, "1-abcdef");
assertThrows(IllegalArgumentException.class, new Executable() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since JUnit5 requires Java 8 anyway, can't we use Java8 features in the test code?
I think this would look a lot better as:

final byte[] bytesToDB = "binary data".getBytes();
final ByteArrayInputStream bytesIn = new ByteArrayInputStream(bytesToDB);
assertThrows(IllegalArgumentException.class, () -> db.saveAttachment(bytesIn, "foo.txt", "text/plain", null, "1-abcdef"));

Even if we don't got that far, just executing the one line expected to throw inside the Executable would be more clear.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are 49 instances of assertThrows and wrapping the whole thing in an Executable was easier, because:

  • That was what the IDE auto-prompted for (lazy, I know)
  • It's difficult to tell with each test which statement is the exact one responsible for throwing the exception, which i guess is one of the drawbacks of the old @Throws annotation.

So it takes time to understand each test and put the assertThrows in the right place, but I agree that the way you have suggested doing it is a lot clearer and more expressive.

Possibly something to look at towards the end of the PR?

}

@Test(expected = IllegalArgumentException.class)
@Test()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

redundant parentheses

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 0b3645d

Copy link
Member

@ricellis ricellis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Phase II, I've nearly made it halfway!

db = dbResource.get();
}
@RequiresDB
public class BulkDocumentTest extends TestWithDb {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couldn't TestWithDb reasonably include the @RequiresDB annotation and avoid the need for it to be present throughout so many test classes. I'm assuming anything that extends TestWithDb also RequiresDB though I guess which DB could be important. Something to think about.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a nice idea but we have test classes like CloudantClientTests which extend TestWithDb but only need the database for a couple of tests, so the whole class need not be marked as @RequiresDB

@@ -47,6 +48,8 @@
try {
//a URL might be supplied, otherwise use the separate properties
String URL = System.getProperty("test.couch.url");
System.out.println(System.getProperties());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you think this and its counterpart on the line below are necessary for debugging can you make it a JUL style thing so we can turn it off if we want. Or if it was just overlooked delete it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd left some debugging in, especially for when test parameters weren't being passed in properly from gradle. This is no longer needed, so I've got rid of all of these in 96ff132

public static void setUp() throws Exception {
account = clientResource.get();
db = dbResource.get();
// TODO before class?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this need doing?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See 1d42697

Per the commit comment "mockWebServer needs to be fetched from extension before each test because the extension is a "per test" extension (overrides beforeEach/afterEach)".

It's a bit wasteful to stop/start the mock web server for each test when currently only one test uses it, but I think the overhead is negligible.

Tom Blench added 3 commits March 13, 2018 10:40
mockWebServer needs to be fetched from extension
before each test because the extension is a "per
test" extension (overrides beforeEach/afterEach)
@emlaver emlaver self-requested a review March 13, 2018 14:39
ByteArrayInputStream bytesIn = new ByteArrayInputStream(bytesToDB);
Response response = db.saveAttachment(bytesIn, "foo.txt", "text/plain", null,
"1-abcdef");
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we consider moving the code in the block above into a helper util method since it occurs several times in this test class?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably outside the scope of what I'm doing, which is just translating the tests to junit5 without considering too closely how these tests are written.

@@ -93,13 +74,14 @@ public void findJsonObject() {
assertNotNull(jsonObject);
}

/*
@Test
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this commented because the test is failing? If so, should we instead use @Disabled?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test wasn't commented out before so if it's failing now we need to fix it. In general though, I suppose this test need not use a Couch specific endpoint, which is perhaps what is pending some thought here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 3e9d231

@emlaver
Copy link
Contributor

emlaver commented Mar 14, 2018

I'm seeing test classes that import org.junit.jupiter.api.Assertions and others that import the utility method such as org.junit.jupiter.api.Assertions.assertEquals. Should we pick one way to import for consistency in all the test classes?

@@ -49,14 +47,14 @@
*/
public class IndexLifecycleTest {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something for a future PR to possibly add clarity in the logs: adding @DisplayName for test classes e.g. @DisplayName("Index lifecycle test...")

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably future PR, but it would be useful

}
private static final String TESTJSON = "{\"_id\":\"literal\"," +
"\"_rev\":\"1-39933759c7250133b6039d94ea09134f\",\"foo\":\"Grüße 日本語 \uD834\uDD1E.\"}\n";
private static final String TESTJSON_ESCAPED = "{\"_id\":\"escaped\"," +
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this have \\ similar to TESTSTRING_ESCAPED above?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for spotting this inconsistency. These are actually used as "expected" values for assertions later, and the json deserialiser will always unescape these values, so they are correct in that regard.

However, the logic and naming of some variables is confusing so I've fixed that in e8a6acb

@@ -878,11 +872,10 @@ public void multiRequest() throws IOException {
.build();
int i = 1;
List<ViewResponse<String, Object>> responses = multi.getViewResponses();
assertEquals("There should be 3 respones for 3 requests", 3, responses.size());
assertEquals(3, responses.size(), "There should be 3 respones for 3 requests");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

respones -> responses

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in f378a13

@@ -999,15 +1013,15 @@ public void multiRequestMixedReduced() throws IOException {
.build();

List<ViewResponse<String, Object>> responses = multi.getViewResponses();
assertEquals("There should be 2 respones for 2 requests", 2, responses.size());
assertEquals(2, responses.size(), "There should be 2 respones for 2 requests");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

respones -> responses

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in f378a13

import org.junit.jupiter.api.extension.RegisterExtension;

// Base class for tests which require a DB which is used throughout the lifetime of the test class
public class TestWithDb {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about having a TestWithDbPerClass that would extend a base TestWithDb class?
TestWithDbPerTest would also extend TestWithDb.

Copy link
Contributor Author

@tomblench tomblench Mar 26, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@emlaver good idea, done in 6be59bb

@@ -0,0 +1,30 @@
package com.cloudant.tests.base;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add copyright

@@ -0,0 +1,35 @@
package com.cloudant.tests.base;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add copyright

Copy link
Member

@ricellis ricellis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Last few comments, now I've completed a first pass.

@@ -93,13 +74,14 @@ public void findJsonObject() {
assertNotNull(jsonObject);
}

/*
@Test
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test wasn't commented out before so if it's failing now we need to fix it. In general though, I suppose this test need not use a Couch specific endpoint, which is perhaps what is pending some thought here?


// Note Parameter(0) okUsable is inherited
// because we fill in the args from the left, we can fill in the single argument "okUsable"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment is a bit confusing. AFAICT the boolean value of okUsable is provided in the Stream of TestTemplateInvocationContexts here and passed into the test method's parameters. I think what this comment is saying is that the resolution of the template parameters precedes the superclass's BeforeEach execution so we can safely provide the boolean this way i.e. the value is set before it is referenced by the superclass?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've just removed it in 5d5abf5 because it was wrong and there's no comment needed here

assertEquals("The results list should be of length 0", 0, db.getViewRequestBuilder
("example", "by_tag").newRequest(Key.Type.STRING, Object.class).keys
("javax").build().getResponse().getKeys().size());
assertEquals(0, db.getViewRequestBuilder("example", "by_tag").newRequest(Key.Type.STRING, Object.class).keys("javax").build().getResponse().getKeys().size(), "The results list should be of length 0");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems like some of these code replacements have produced very long lines. Perhaps at the end of the review process it is just worth applying the formatter to all the classes before the final merge?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea, i've added it to the TODOs

@@ -0,0 +1,60 @@
/*
* Copyright © 2017 IBM Corp. All rights reserved.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As you said in the TODOs description for this PR there are a bunch of copyright updates to do.

import org.junit.jupiter.api.extension.Extension;
import org.junit.jupiter.api.extension.ExtensionContext;

public class MultiExtension implements Extension, AfterAllCallback, AfterEachCallback,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remind me why this is necessary, was it that @RegisterExtension doesn't currently guarantee an order and @ExtendWith doesn't allow arguments to be passed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note some files have had line endings converted to UNIX in the process
Copy link
Member

@ricellis ricellis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1
Don't forget to remove the Jenkinsfile junit branch name check.
Also I think it would be good to open one or more follow up issues for these points that didn't make it into this:
i) Improve test tagging
ii) Throws assertions in java 8 style
iii) AttachmentsTest refactor
iv) @DisplayName

Thanks

Copy link
Contributor

@emlaver emlaver left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good. I'm also +1 on future work item for improving test tagging.

@tomblench tomblench merged commit 3529583 into master Mar 28, 2018
@ricellis ricellis deleted the junit5 branch April 24, 2018 12:30
@ricellis ricellis added this to the 2.next milestone Apr 25, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants