Repository: jhy/jsoup
Pull request #1335 (2020-03-04 16:22:35)

Java8 and JUnit5
Update to Java 8 and JUnit 5.

While porting the tests and assertions to JUnit 5 wasn't difficult, the `@Rule`'s were tricky as they are no longer supported. I've implemented tests using the former locale rule as parametrized tests, which provide the benefit of test results for each locale. This has a drawback unfortunately: JUnit passes the parameters as argument, so the test must set the locale at the beginning – I haven't figured out how to automate this yet; the default locale is restored automatically however.

@jhy there's an unused test case ([`ElementTest::testChildSizeWithMixedContent()`](https://github.com/jhy/jsoup/blob/master/src/test/java/org/jsoup/nodes/ElementTest.java#L1599) which isn't marked with `@Test` nor used anywhere else. Is this intentional? It looks like accidental to me.


Patch:
diff --git a/.travis.yml b/.travis.yml
index 23100bba..988bd1b8 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -13,7 +13,6 @@ env:
     # List any JDK you want to build your software with.
     # You can see the list of supported environments by installing Jabba and using ls-remote:
     # https://github.com/shyiko/jabba#usage
-    - JDK="openjdk-ri@1.7."
     - JDK="adopt@1.8."
     - JDK="amazon-corretto@1.8."
     - JDK="openjdk@1.9."
diff --git a/pom.xml b/pom.xml
index 19034a2f..b691ec7f 100644
--- a/pom.xml
+++ b/pom.xml
@@ -38,8 +38,8 @@
         <artifactId>maven-compiler-plugin</artifactId>
         <version>3.8.0</version>
         <configuration>
-          <source>1.7</source>
-          <target>1.7</target>
+          <source>1.8</source>
+          <target>1.8</target>
           <encoding>UTF-8</encoding>
           <compilerArgs>
             <!-- saves output for package-info.java, so mvn sees it has completed it, so incremental compile works -->
@@ -50,7 +50,7 @@
         </configuration>
       </plugin>
       <plugin>
-        <!-- this plugin allows us to ensure Java 7 API compatibility -->
+        <!-- this plugin allows us to ensure Java 8 API compatibility -->
         <groupId>org.codehaus.mojo</groupId>
         <artifactId>animal-sniffer-maven-plugin</artifactId>
         <version>1.16</version>
@@ -64,7 +64,7 @@
             <configuration>
               <signature>
                 <groupId>org.codehaus.mojo.signature</groupId>
-                <artifactId>java17</artifactId>
+                <artifactId>java18</artifactId>
                 <version>1.0</version>
               </signature>
               <signature>
@@ -157,6 +157,11 @@
         <artifactId>maven-release-plugin</artifactId>
         <version>2.5.3</version>
       </plugin>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-surefire-plugin</artifactId>
+        <version>3.0.0-M3</version>
+      </plugin>
       <plugin>
         <artifactId>maven-failsafe-plugin</artifactId>
         <version>3.0.0-M3</version>
@@ -251,11 +256,23 @@
 
   <dependencies>
 
+    <!-- junit -->
+    <dependency>
+      <groupId>org.junit.jupiter</groupId>
+      <artifactId>junit-jupiter-api</artifactId>
+      <version>5.6.0</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.junit.jupiter</groupId>
+      <artifactId>junit-jupiter-engine</artifactId>
+      <version>5.6.0</version>
+      <scope>test</scope>
+    </dependency>
     <dependency>
-      <!-- junit -->
-      <groupId>junit</groupId>
-      <artifactId>junit</artifactId>
-      <version>4.12</version>
+      <groupId>org.junit.jupiter</groupId>
+      <artifactId>junit-jupiter-params</artifactId>
+      <version>5.6.0</version>
       <scope>test</scope>
     </dependency>
 
diff --git a/src/main/java/org/jsoup/parser/CharacterReader.java b/src/main/java/org/jsoup/parser/CharacterReader.java
index ef1590bc..d4f0d2b8 100644
--- a/src/main/java/org/jsoup/parser/CharacterReader.java
+++ b/src/main/java/org/jsoup/parser/CharacterReader.java
@@ -311,6 +311,32 @@ public final class CharacterReader {
         return pos > start ? cacheString(charBuf, stringCache, start, pos -start) : "";
     }
 
+    String consumeAttributeQuoted(final boolean single) {
+        // null, " or ', &
+        //bufferUp(); // no need to bufferUp, just called consume()
+        int pos = bufPos;
+        final int start = pos;
+        final int remaining = bufLength;
+        final char[] val = charBuf;
+
+        OUTER: while (pos < remaining) {
+            switch (val[pos]) {
+                case '&':
+                case TokeniserState.nullChar:
+                    break OUTER;
+                case '\'':
+                    if (single) break OUTER;
+                case '"':
+                    if (!single) break OUTER;;
+                default:
+                    pos++;
+            }
+        }
+        bufPos = pos;
+        return pos > start ? cacheString(charBuf, stringCache, start, pos -start) : "";
+    }
+
+
     String consumeRawData() {
         // <, null
         //bufferUp(); // no need to bufferUp, just called consume()
diff --git a/src/main/java/org/jsoup/parser/Tokeniser.java b/src/main/java/org/jsoup/parser/Tokeniser.java
index 788d8bd8..6d2e8445 100644
--- a/src/main/java/org/jsoup/parser/Tokeniser.java
+++ b/src/main/java/org/jsoup/parser/Tokeniser.java
@@ -104,6 +104,31 @@ final class Tokeniser {
         }
     }
 
+    // variations to limit need to create temp strings
+    void emit(final StringBuilder str) {
+        if (charsString == null) {
+            charsString = str.toString();
+        }
+        else {
+            if (charsBuilder.length() == 0) {
+                charsBuilder.append(charsString);
+            }
+            charsBuilder.append(str);
+        }
+    }
+
+    void emit(char c) {
+        if (charsString == null) {
+            charsString = String.valueOf(c);
+        }
+        else {
+            if (charsBuilder.length() == 0) {
+                charsBuilder.append(charsString);
+            }
+            charsBuilder.append(c);
+        }
+    }
+
     void emit(char[] chars) {
         emit(String.valueOf(chars));
     }
@@ -112,10 +137,6 @@ final class Tokeniser {
         emit(new String(codepoints, 0, codepoints.length));
     }
 
-    void emit(char c) {
-        emit(String.valueOf(c));
-    }
-
     TokeniserState getState() {
         return state;
     }
diff --git a/src/main/java/org/jsoup/parser/TokeniserState.java b/src/main/java/org/jsoup/parser/TokeniserState.java
index d81cecce..74e96fa5 100644
--- a/src/main/java/org/jsoup/parser/TokeniserState.java
+++ b/src/main/java/org/jsoup/parser/TokeniserState.java
@@ -253,7 +253,8 @@ enum TokeniserState {
         }
 
         private void anythingElse(Tokeniser t, CharacterReader r) {
-            t.emit("</" + t.dataBuffer.toString());
+            t.emit("</");
+            t.emit(t.dataBuffer);
             r.unconsume();
             t.transition(Rcdata);
         }
@@ -423,7 +424,8 @@ enum TokeniserState {
             if (r.matchesLetter()) {
                 t.createTempBuffer();
                 t.dataBuffer.append(r.current());
-                t.emit("<" + r.current());
+                t.emit("<");
+                t.emit(r.current());
                 t.advanceTransition(ScriptDataDoubleEscapeStart);
             } else if (r.matches('/')) {
                 t.createTempBuffer();
@@ -744,7 +746,7 @@ enum TokeniserState {
     },
     AttributeValue_doubleQuoted {
         void read(Tokeniser t, CharacterReader r) {
-            String value = r.consumeToAnySorted(attributeDoubleValueCharsSorted);
+            String value = r.consumeAttributeQuoted(false);
             if (value.length() > 0)
                 t.tagPending.appendAttributeValue(value);
             else
@@ -777,7 +779,7 @@ enum TokeniserState {
     },
     AttributeValue_singleQuoted {
         void read(Tokeniser t, CharacterReader r) {
-            String value = r.consumeToAnySorted(attributeSingleValueCharsSorted);
+            String value = r.consumeAttributeQuoted(true);
             if (value.length() > 0)
                 t.tagPending.appendAttributeValue(value);
             else
@@ -1630,8 +1632,6 @@ enum TokeniserState {
 
     static final char nullChar = '\u0000';
     // char searches. must be sorted, used in inSorted. MUST update TokenisetStateTest if more arrays are added.
-    static final char[] attributeSingleValueCharsSorted = new char[]{nullChar, '&', '\''};
-    static final char[] attributeDoubleValueCharsSorted = new char[]{nullChar, '"', '&'};
     static final char[] attributeNameCharsSorted = new char[]{nullChar, '\t', '\n', '\f', '\r', ' ', '"', '\'', '/', '<', '=', '>'};
     static final char[] attributeValueUnquoted = new char[]{nullChar, '\t', '\n', '\f', '\r', ' ', '"', '&', '\'', '<', '=', '>', '`'};
 
@@ -1678,7 +1678,8 @@ enum TokeniserState {
         }
 
         if (needsExitTransition) {
-            t.emit("</" + t.dataBuffer.toString());
+            t.emit("</");
+            t.emit(t.dataBuffer);
             t.transition(elseTransition);
         }
     }


Test patch:
diff --git a/src/test/java/org/jsoup/MultiLocaleExtension.java b/src/test/java/org/jsoup/MultiLocaleExtension.java
new file mode 100644
index 00000000..7ce23619
--- /dev/null
+++ b/src/test/java/org/jsoup/MultiLocaleExtension.java
@@ -0,0 +1,38 @@
+package org.jsoup;
+
+import org.junit.jupiter.api.extension.AfterEachCallback;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.extension.ExtensionContext;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.ArgumentsProvider;
+import org.junit.jupiter.params.provider.ArgumentsSource;
+
+import java.lang.annotation.*;
+import java.util.Locale;
+import java.util.stream.Stream;
+
+public class MultiLocaleExtension implements AfterEachCallback, ArgumentsProvider {
+    private final Locale defaultLocale = Locale.getDefault();
+
+    @Override
+    public void afterEach(ExtensionContext context) {
+        Locale.setDefault(defaultLocale);
+    }
+
+    @Override
+    public Stream<? extends Arguments> provideArguments(ExtensionContext extensionContext) {
+        return Stream.of(Arguments.of(Locale.ENGLISH), Arguments.arguments(new Locale("tr")));
+    }
+
+
+    @Documented
+    @Target(ElementType.METHOD)
+    @Retention(RetentionPolicy.RUNTIME)
+    @ArgumentsSource(MultiLocaleExtension.class)
+    @ExtendWith(MultiLocaleExtension.class)
+    @ParameterizedTest
+    public @interface MultiLocaleTest {
+    }
+
+}
diff --git a/src/test/java/org/jsoup/MultiLocaleRule.java b/src/test/java/org/jsoup/MultiLocaleRule.java
deleted file mode 100644
index 4e63f09c..00000000
--- a/src/test/java/org/jsoup/MultiLocaleRule.java
+++ /dev/null
@@ -1,42 +0,0 @@
-package org.jsoup;
-
-import org.junit.rules.TestRule;
-import org.junit.runner.Description;
-import org.junit.runners.model.Statement;
-
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.util.Locale;
-
-public class MultiLocaleRule implements TestRule {
-    @Retention(RetentionPolicy.RUNTIME)
-    public @interface MultiLocaleTest {
-    }
-
-    @Override
-    public Statement apply(final Statement statement, final Description description) {
-        return new Statement() {
-            @Override
-            public void evaluate() throws Throwable {
-                MultiLocaleTest annotation = description.getAnnotation(MultiLocaleTest.class);
-                if (annotation == null) {
-                    statement.evaluate();
-                    return;
-                }
-
-                evaluateWithLocale(Locale.ENGLISH);
-                evaluateWithLocale(new Locale("tr"));
-            }
-
-            private void evaluateWithLocale(Locale locale) throws Throwable {
-                Locale oldLocale = Locale.getDefault();
-                Locale.setDefault(locale);
-                try {
-                    statement.evaluate();
-                } finally {
-                    Locale.setDefault(oldLocale);
-                }
-            }
-        };
-    }
-}
diff --git a/src/test/java/org/jsoup/helper/DataUtilTest.java b/src/test/java/org/jsoup/helper/DataUtilTest.java
index 32ecd5fa..f585bb26 100644
--- a/src/test/java/org/jsoup/helper/DataUtilTest.java
+++ b/src/test/java/org/jsoup/helper/DataUtilTest.java
@@ -3,20 +3,13 @@ package org.jsoup.helper;
 import org.jsoup.Jsoup;
 import org.jsoup.nodes.Document;
 import org.jsoup.parser.Parser;
-import org.junit.Test;
+import org.junit.jupiter.api.Test;
 
-import java.io.ByteArrayInputStream;
-import java.io.File;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.UnsupportedEncodingException;
+import java.io.*;
 import java.nio.charset.StandardCharsets;
 
 import static org.jsoup.integration.ParseTest.getFile;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotSame;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
+import static org.junit.jupiter.api.Assertions.*;
 
 public class DataUtilTest {
     @Test
@@ -24,16 +17,17 @@ public class DataUtilTest {
         assertEquals("utf-8", DataUtil.getCharsetFromContentType("text/html;charset=utf-8 "));
         assertEquals("UTF-8", DataUtil.getCharsetFromContentType("text/html; charset=UTF-8"));
         assertEquals("ISO-8859-1", DataUtil.getCharsetFromContentType("text/html; charset=ISO-8859-1"));
-        assertEquals(null, DataUtil.getCharsetFromContentType("text/html"));
-        assertEquals(null, DataUtil.getCharsetFromContentType(null));
-        assertEquals(null, DataUtil.getCharsetFromContentType("text/html;charset=Unknown"));
+        assertNull(DataUtil.getCharsetFromContentType("text/html"));
+        assertNull(DataUtil.getCharsetFromContentType(null));
+        assertNull(DataUtil.getCharsetFromContentType("text/html;charset=Unknown"));
     }
 
-    @Test public void testQuotedCharset() {
+    @Test
+    public void testQuotedCharset() {
         assertEquals("utf-8", DataUtil.getCharsetFromContentType("text/html; charset=\"utf-8\""));
         assertEquals("UTF-8", DataUtil.getCharsetFromContentType("text/html;charset=\"UTF-8\""));
         assertEquals("ISO-8859-1", DataUtil.getCharsetFromContentType("text/html; charset=\"ISO-8859-1\""));
-        assertEquals(null, DataUtil.getCharsetFromContentType("text/html; charset=\"Unsupported\""));
+        assertNull(DataUtil.getCharsetFromContentType("text/html; charset=\"Unsupported\""));
         assertEquals("UTF-8", DataUtil.getCharsetFromContentType("text/html; charset='UTF-8'"));
     }
 
@@ -50,13 +44,15 @@ public class DataUtilTest {
         return null;
     }
 
-    @Test public void discardsSpuriousByteOrderMark() throws IOException {
+    @Test
+    public void discardsSpuriousByteOrderMark() throws IOException {
         String html = "\uFEFF<html><head><title>One</title></head><body>Two</body></html>";
         Document doc = DataUtil.parseInputStream(stream(html), "UTF-8", "http://foo.com/", Parser.htmlParser());
         assertEquals("One", doc.head().text());
     }
 
-    @Test public void discardsSpuriousByteOrderMarkWhenNoCharsetSet() throws IOException {
+    @Test
+    public void discardsSpuriousByteOrderMarkWhenNoCharsetSet() throws IOException {
         String html = "\uFEFF<html><head><title>One</title></head><body>Two</body></html>";
         Document doc = DataUtil.parseInputStream(stream(html), null, "http://foo.com/", Parser.htmlParser());
         assertEquals("One", doc.head().text());
@@ -65,8 +61,8 @@ public class DataUtilTest {
 
     @Test
     public void shouldNotThrowExceptionOnEmptyCharset() {
-        assertEquals(null, DataUtil.getCharsetFromContentType("text/html; charset="));
-        assertEquals(null, DataUtil.getCharsetFromContentType("text/html; charset=;"));
+        assertNull(DataUtil.getCharsetFromContentType("text/html; charset="));
+        assertNull(DataUtil.getCharsetFromContentType("text/html; charset=;"));
     }
 
     @Test
@@ -81,7 +77,7 @@ public class DataUtilTest {
 
     @Test
     public void shouldReturnNullForIllegalCharsetNames() {
-        assertEquals(null, DataUtil.getCharsetFromContentType("text/html; charset=$HJKDF§$/("));
+        assertNull(DataUtil.getCharsetFromContentType("text/html; charset=$HJKDF§$/("));
     }
 
     @Test
@@ -93,7 +89,7 @@ public class DataUtilTest {
         assertEquals(DataUtil.boundaryLength, m2.length());
         assertNotSame(m1, m2);
     }
-    
+
     @Test
     public void wrongMetaCharsetFallback() throws IOException {
         String html = "<html><head><meta charset=iso-8></head><body></body></html>";
@@ -101,11 +97,11 @@ public class DataUtilTest {
         Document doc = DataUtil.parseInputStream(stream(html), null, "http://example.com", Parser.htmlParser());
 
         final String expected = "<html>\n" +
-            " <head>\n" +
-            "  <meta charset=\"iso-8\">\n" +
-            " </head>\n" +
-            " <body></body>\n" +
-            "</html>";
+                " <head>\n" +
+                "  <meta charset=\"iso-8\">\n" +
+                " </head>\n" +
+                " <body></body>\n" +
+                "</html>";
 
         assertEquals(expected, doc.toString());
     }
@@ -177,9 +173,9 @@ public class DataUtilTest {
     public void supportsXmlCharsetDeclaration() throws IOException {
         String encoding = "iso-8859-1";
         InputStream soup = new ByteArrayInputStream((
-            "<?xml version=\"1.0\" encoding=\"iso-8859-1\"?>" +
-                "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\">" +
-                "<html xmlns=\"http://www.w3.org/1999/xhtml\" lang=\"en\" xml:lang=\"en\">Hellö Wörld!</html>"
+                "<?xml version=\"1.0\" encoding=\"iso-8859-1\"?>" +
+                        "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\">" +
+                        "<html xmlns=\"http://www.w3.org/1999/xhtml\" lang=\"en\" xml:lang=\"en\">Hellö Wörld!</html>"
         ).getBytes(encoding));
 
         Document doc = Jsoup.parse(soup, null, "");
@@ -187,14 +183,16 @@ public class DataUtilTest {
     }
 
 
-    @Test public void lLoadsGzipFile() throws IOException {
+    @Test
+    public void lLoadsGzipFile() throws IOException {
         File in = getFile("/htmltests/gzip.html.gz");
         Document doc = Jsoup.parse(in, null);
         assertEquals("Gzip test", doc.title());
         assertEquals("This is a gzipped HTML file.", doc.selectFirst("p").text());
     }
 
-    @Test public void loadsZGzipFile() throws IOException {
+    @Test
+    public void loadsZGzipFile() throws IOException {
         // compressed on win, with z suffix
         File in = getFile("/htmltests/gzip.html.z");
         Document doc = Jsoup.parse(in, null);
@@ -202,7 +200,8 @@ public class DataUtilTest {
         assertEquals("This is a gzipped HTML file.", doc.selectFirst("p").text());
     }
 
-    @Test public void handlesFakeGzipFile () throws IOException {
+    @Test
+    public void handlesFakeGzipFile() throws IOException {
         File in = getFile("/htmltests/fake-gzip.html.gz");
         Document doc = Jsoup.parse(in, null);
         assertEquals("This is not gzipped", doc.title());
diff --git a/src/test/java/org/jsoup/helper/HttpConnectionTest.java b/src/test/java/org/jsoup/helper/HttpConnectionTest.java
index 896383d8..2b85b921 100644
--- a/src/test/java/org/jsoup/helper/HttpConnectionTest.java
+++ b/src/test/java/org/jsoup/helper/HttpConnectionTest.java
@@ -1,48 +1,45 @@
 package org.jsoup.helper;
 
 import org.jsoup.Connection;
-import org.jsoup.Jsoup;
-import org.jsoup.MultiLocaleRule;
-import org.jsoup.MultiLocaleRule.MultiLocaleTest;
+import org.jsoup.MultiLocaleExtension.MultiLocaleTest;
 import org.jsoup.integration.ParseTest;
-import org.junit.Rule;
-import org.junit.Test;
+import org.junit.jupiter.api.Test;
 
 import java.io.IOException;
 import java.net.MalformedURLException;
 import java.net.URL;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
+import java.util.*;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
+import static org.junit.jupiter.api.Assertions.*;
 
 public class HttpConnectionTest {
     /* most actual network http connection tests are in integration */
 
-    @Rule public MultiLocaleRule rule = new MultiLocaleRule();
-
-    @Test(expected=IllegalArgumentException.class) public void throwsExceptionOnParseWithoutExecute() throws IOException {
-        Connection con = HttpConnection.connect("http://example.com");
-        con.response().parse();
+    @Test public void throwsExceptionOnParseWithoutExecute() {
+        assertThrows(IllegalArgumentException.class, () -> {
+            Connection con = HttpConnection.connect("http://example.com");
+            con.response().parse();
+        });
     }
 
-    @Test(expected=IllegalArgumentException.class) public void throwsExceptionOnBodyWithoutExecute() throws IOException {
-        Connection con = HttpConnection.connect("http://example.com");
-        con.response().body();
+    @Test public void throwsExceptionOnBodyWithoutExecute() {
+        assertThrows(IllegalArgumentException.class, () -> {
+            Connection con = HttpConnection.connect("http://example.com");
+            con.response().body();
+        });
     }
 
-    @Test(expected=IllegalArgumentException.class) public void throwsExceptionOnBodyAsBytesWithoutExecute() throws IOException {
-        Connection con = HttpConnection.connect("http://example.com");
-        con.response().bodyAsBytes();
+    @Test public void throwsExceptionOnBodyAsBytesWithoutExecute() {
+        assertThrows(IllegalArgumentException.class, () -> {
+            Connection con = HttpConnection.connect("http://example.com");
+            con.response().bodyAsBytes();
+        });
     }
 
-    @Test @MultiLocaleTest public void caseInsensitiveHeaders() {
+    @MultiLocaleTest
+    public void caseInsensitiveHeaders(Locale locale) {
+        Locale.setDefault(locale);
+
         Connection.Response res = new HttpConnection.Response();
         res.header("Accept-Encoding", "gzip");
         res.header("content-type", "text/html");
@@ -122,13 +119,13 @@ public class HttpConnectionTest {
         headers = req.multiHeaders();
         assertEquals("Bar", headers.get("Foo").get(0));
         assertFalse(req.hasHeader("Accept"));
-        assertTrue(headers.get("Accept") == null);
+        assertNull(headers.get("Accept"));
     }
 
     @Test public void ignoresEmptySetCookies() {
         // prep http response header map
         Map<String, List<String>> headers = new HashMap<>();
-        headers.put("Set-Cookie", Collections.<String>emptyList());
+        headers.put("Set-Cookie", Collections.emptyList());
         HttpConnection.Response res = new HttpConnection.Response();
         res.processResponseHeaders(headers);
         assertEquals(0, res.cookies().size());
@@ -160,8 +157,8 @@ public class HttpConnectionTest {
         assertEquals("http://example.com", con.request().url().toExternalForm());
     }
 
-    @Test(expected=IllegalArgumentException.class) public void throwsOnMalformedUrl() {
-        Connection con = HttpConnection.connect("bzzt");
+    @Test public void throwsOnMalformedUrl() {
+        assertThrows(IllegalArgumentException.class, () -> HttpConnection.connect("bzzt"));
     }
 
     @Test public void userAgent() {
@@ -191,9 +188,11 @@ public class HttpConnectionTest {
         assertEquals(Connection.Method.POST, con.request().method());
     }
 
-    @Test(expected=IllegalArgumentException.class) public void throwsOnOddData() {
-        Connection con = HttpConnection.connect("http://example.com/");
-        con.data("Name", "val", "what");
+    @Test public void throwsOnOddData() {
+        assertThrows(IllegalArgumentException.class, () -> {
+            Connection con = HttpConnection.connect("http://example.com/");
+            con.data("Name", "val", "what");
+        });
     }
 
     @Test public void data() {
diff --git a/src/test/java/org/jsoup/helper/W3CDomTest.java b/src/test/java/org/jsoup/helper/W3CDomTest.java
index 6b84e524..02bf00fe 100644
--- a/src/test/java/org/jsoup/helper/W3CDomTest.java
+++ b/src/test/java/org/jsoup/helper/W3CDomTest.java
@@ -4,14 +4,12 @@ import org.jsoup.Jsoup;
 import org.jsoup.TextUtil;
 import org.jsoup.integration.ParseTest;
 import org.jsoup.nodes.Element;
-import org.junit.Test;
+import org.junit.jupiter.api.Test;
 import org.w3c.dom.Document;
 import org.w3c.dom.DocumentType;
 import org.w3c.dom.Node;
 import org.w3c.dom.NodeList;
-import org.xml.sax.EntityResolver;
 import org.xml.sax.InputSource;
-import org.xml.sax.SAXException;
 
 import javax.xml.parsers.DocumentBuilder;
 import javax.xml.parsers.DocumentBuilderFactory;
@@ -27,7 +25,7 @@ import java.io.StringReader;
 import java.nio.charset.StandardCharsets;
 import java.util.Map;
 
-import static org.junit.Assert.*;
+import static org.junit.jupiter.api.Assertions.*;
 
 public class W3CDomTest {
 
@@ -36,15 +34,11 @@ public class W3CDomTest {
             DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
             factory.setNamespaceAware(nameSpaceAware);
             DocumentBuilder builder = factory.newDocumentBuilder();
-            builder.setEntityResolver(new EntityResolver() {
-                @Override
-                public InputSource resolveEntity(String publicId, String systemId)
-                    throws SAXException, IOException {
-                    if (systemId.contains("about:legacy-compat")) { // <!doctype html>
-                        return new InputSource(new StringReader(""));
-                    } else {
-                        return null;
-                    }
+            builder.setEntityResolver((publicId, systemId) -> {
+                if (systemId.contains("about:legacy-compat")) { // <!doctype html>
+                    return new InputSource(new StringReader(""));
+                } else {
+                    return null;
                 }
             });
             Document dom = builder.parse(new ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8)));
diff --git a/src/test/java/org/jsoup/integration/ConnectIT.java b/src/test/java/org/jsoup/integration/ConnectIT.java
index 19bfb8f7..4e74c35b 100644
--- a/src/test/java/org/jsoup/integration/ConnectIT.java
+++ b/src/test/java/org/jsoup/integration/ConnectIT.java
@@ -5,13 +5,13 @@ import org.jsoup.Jsoup;
 import org.jsoup.integration.servlets.SlowRider;
 import org.jsoup.nodes.Document;
 import org.jsoup.nodes.Element;
-import org.junit.Test;
+import org.junit.jupiter.api.Test;
 
 import java.io.IOException;
 import java.net.SocketTimeoutException;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 
 /**
  * Failsafe integration tests for Connect methods. These take a bit longer to run, so included as Integ, not Unit, tests.
@@ -22,18 +22,16 @@ public class ConnectIT {
     public void canInterruptBodyStringRead() throws InterruptedException {
         // todo - implement in interruptable channels, so it's immediate
         final String[] body = new String[1];
-        Thread runner = new Thread(new Runnable() {
-            public void run() {
-                try {
-                    Connection.Response res = Jsoup.connect(SlowRider.Url)
-                        .timeout(15 * 1000)
-                        .execute();
-                    body[0] = res.body();
-                } catch (IOException e) {
-                    throw new RuntimeException(e);
-                }
-
+        Thread runner = new Thread(() -> {
+            try {
+                Connection.Response res = Jsoup.connect(SlowRider.Url)
+                    .timeout(15 * 1000)
+                    .execute();
+                body[0] = res.body();
+            } catch (IOException e) {
+                throw new RuntimeException(e);
             }
+
         });
 
         runner.start();
@@ -50,18 +48,16 @@ public class ConnectIT {
     public void canInterruptDocumentRead() throws InterruptedException {
         // todo - implement in interruptable channels, so it's immediate
         final String[] body = new String[1];
-        Thread runner = new Thread(new Runnable() {
-            public void run() {
-                try {
-                    Connection.Response res = Jsoup.connect(SlowRider.Url)
-                        .timeout(15 * 1000)
-                        .execute();
-                    body[0] = res.parse().text();
-                } catch (IOException e) {
-                    throw new RuntimeException(e);
-                }
-
+        Thread runner = new Thread(() -> {
+            try {
+                Connection.Response res = Jsoup.connect(SlowRider.Url)
+                    .timeout(15 * 1000)
+                    .execute();
+                body[0] = res.parse().text();
+            } catch (IOException e) {
+                throw new RuntimeException(e);
             }
+
         });
 
         runner.start();
@@ -83,8 +79,8 @@ public class ConnectIT {
         } catch (SocketTimeoutException e) {
             long end = System.currentTimeMillis();
             long took = end - start;
-            assertTrue(("Time taken was " + took), took > timeout);
-            assertTrue(("Time taken was " + took), took < timeout * 1.8);
+            assertTrue(took > timeout, ("Time taken was " + took));
+            assertTrue(took < timeout * 1.8, ("Time taken was " + took));
             threw = true;
         }
 
diff --git a/src/test/java/org/jsoup/integration/ConnectTest.java b/src/test/java/org/jsoup/integration/ConnectTest.java
index 7b612ddd..663ad448 100644
--- a/src/test/java/org/jsoup/integration/ConnectTest.java
+++ b/src/test/java/org/jsoup/integration/ConnectTest.java
@@ -3,17 +3,12 @@ package org.jsoup.integration;
 import org.jsoup.Connection;
 import org.jsoup.HttpStatusException;
 import org.jsoup.Jsoup;
-import org.jsoup.integration.servlets.Deflateservlet;
-import org.jsoup.integration.servlets.EchoServlet;
-import org.jsoup.integration.servlets.FileServlet;
-import org.jsoup.integration.servlets.HelloServlet;
-import org.jsoup.integration.servlets.InterruptedServlet;
-import org.jsoup.integration.servlets.RedirectServlet;
+import org.jsoup.integration.servlets.*;
 import org.jsoup.nodes.Document;
 import org.jsoup.nodes.Element;
-import org.junit.AfterClass;
-import org.junit.BeforeClass;
-import org.junit.Test;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
 
 import java.io.File;
 import java.io.FileInputStream;
@@ -25,7 +20,7 @@ import java.util.Map;
 import static org.jsoup.helper.HttpConnection.CONTENT_TYPE;
 import static org.jsoup.helper.HttpConnection.MULTIPART_FORM_DATA;
 import static org.jsoup.integration.UrlConnectTest.browserUa;
-import static org.junit.Assert.*;
+import static org.junit.jupiter.api.Assertions.*;
 
 /**
  * Tests Jsoup.connect against a local server.
@@ -33,13 +28,13 @@ import static org.junit.Assert.*;
 public class ConnectTest {
     private static String echoUrl;
 
-    @BeforeClass
+    @BeforeAll
     public static void setUp() {
         TestServer.start();
         echoUrl = EchoServlet.Url;
     }
 
-    @AfterClass
+    @AfterAll
     public static void tearDown() {
         TestServer.stop();
     }
@@ -270,7 +265,8 @@ public class ConnectTest {
          */
     }
 
-    @Test public void multipleParsesOkAfterBufferUp() throws IOException {
+    @Test
+    public void multipleParsesOkAfterBufferUp() throws IOException {
         Connection.Response res = Jsoup.connect(echoUrl).execute().bufferUp();
 
         Document doc = res.parse();
@@ -280,13 +276,17 @@ public class ConnectTest {
         assertTrue(doc2.title().contains("Environment"));
     }
 
-    @Test(expected=IllegalArgumentException.class) public void bodyAfterParseThrowsValidationError() throws IOException {
-        Connection.Response res = Jsoup.connect(echoUrl).execute();
-        Document doc = res.parse();
-        String body = res.body();
+    @Test
+    public void bodyAfterParseThrowsValidationError() {
+        assertThrows(IllegalArgumentException.class, () -> {
+            Connection.Response res = Jsoup.connect(echoUrl).execute();
+            Document doc = res.parse();
+            String body = res.body();
+        });
     }
 
-    @Test public void bodyAndBytesAvailableBeforeParse() throws IOException {
+    @Test
+    public void bodyAndBytesAvailableBeforeParse() throws IOException {
         Connection.Response res = Jsoup.connect(echoUrl).execute();
         String body = res.body();
         assertTrue(body.contains("Environment"));
@@ -297,11 +297,14 @@ public class ConnectTest {
         assertTrue(doc.title().contains("Environment"));
     }
 
-    @Test(expected=IllegalArgumentException.class) public void parseParseThrowsValidates() throws IOException {
-        Connection.Response res = Jsoup.connect(echoUrl).execute();
-        Document doc = res.parse();
-        assertTrue(doc.title().contains("Environment"));
-        Document doc2 = res.parse(); // should blow up because the response input stream has been drained
+    @Test
+    public void parseParseThrowsValidates() {
+        assertThrows(IllegalArgumentException.class, () -> {
+            Connection.Response res = Jsoup.connect(echoUrl).execute();
+            Document doc = res.parse();
+            assertTrue(doc.title().contains("Environment"));
+            Document doc2 = res.parse(); // should blow up because the response input stream has been drained
+        });
     }
 
 
diff --git a/src/test/java/org/jsoup/integration/ParseTest.java b/src/test/java/org/jsoup/integration/ParseTest.java
index 7e8c8f1d..f3c1dfcb 100644
--- a/src/test/java/org/jsoup/integration/ParseTest.java
+++ b/src/test/java/org/jsoup/integration/ParseTest.java
@@ -7,7 +7,7 @@ import org.jsoup.nodes.Element;
 import org.jsoup.parser.ParseErrorList;
 import org.jsoup.parser.Parser;
 import org.jsoup.select.Elements;
-import org.junit.Test;
+import org.junit.jupiter.api.Test;
 
 import java.io.*;
 import java.net.URISyntaxException;
@@ -17,7 +17,7 @@ import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
 import java.util.zip.GZIPInputStream;
 
-import static org.junit.Assert.*;
+import static org.junit.jupiter.api.Assertions.*;
 
 /**
  * Integration test: parses from real-world example HTML.
diff --git a/src/test/java/org/jsoup/integration/UrlConnectTest.java b/src/test/java/org/jsoup/integration/UrlConnectTest.java
index e27388a7..b9a6deac 100644
--- a/src/test/java/org/jsoup/integration/UrlConnectTest.java
+++ b/src/test/java/org/jsoup/integration/UrlConnectTest.java
@@ -3,15 +3,15 @@ package org.jsoup.integration;
 import org.jsoup.Connection;
 import org.jsoup.Jsoup;
 import org.jsoup.UnsupportedMimeTypeException;
-import org.jsoup.internal.StringUtil;
 import org.jsoup.helper.W3CDom;
+import org.jsoup.internal.StringUtil;
 import org.jsoup.nodes.Document;
 import org.jsoup.nodes.FormElement;
 import org.jsoup.parser.HtmlTreeBuilder;
 import org.jsoup.parser.Parser;
 import org.jsoup.parser.XmlTreeBuilder;
-import org.junit.Ignore;
-import org.junit.Test;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
 
 import java.io.File;
 import java.io.FileInputStream;
@@ -22,13 +22,13 @@ import java.net.Proxy;
 import java.net.URL;
 import java.util.List;
 
-import static org.junit.Assert.*;
+import static org.junit.jupiter.api.Assertions.*;
 
 /**
  Tests the URL connection. Not enabled by default, so tests don't require network connection.
 
  @author Jonathan Hedley, jonathan@hedley.net */
-@Ignore // ignored by default so tests don't require network access. comment out to enable.
+@Disabled // ignored by default so tests don't require network access. comment out to enable.
 // todo: rebuild these into a local Jetty test server, so not reliant on the vagaries of the internet.
 public class UrlConnectTest {
     private static final String WEBSITE_WITH_INVALID_CERTIFICATE = "https://certs.cac.washington.edu/CAtest/";
@@ -256,10 +256,10 @@ public class UrlConnectTest {
      *
      * @throws Exception
      */
-    @Test(expected = IOException.class)
+    @Test
     public void testUnsafeFail() throws Exception {
         String url = WEBSITE_WITH_INVALID_CERTIFICATE;
-        Jsoup.connect(url).execute();
+        assertThrows(IOException.class, () -> Jsoup.connect(url).execute());
     }
 
 
@@ -272,9 +272,9 @@ public class UrlConnectTest {
      * Test is ignored independent from others as it requires JDK 1.6
      * @throws Exception
      */
-    @Test(expected = IOException.class)
+    @Test
     public void testSNIFail() throws Exception {
-        Jsoup.connect(WEBSITE_WITH_SNI).execute();
+        assertThrows(IOException.class, () -> Jsoup.connect(WEBSITE_WITH_SNI).execute());
     }
 
     @Test
diff --git a/src/test/java/org/jsoup/integration/servlets/Deflateservlet.java b/src/test/java/org/jsoup/integration/servlets/Deflateservlet.java
index f131d660..13af9162 100644
--- a/src/test/java/org/jsoup/integration/servlets/Deflateservlet.java
+++ b/src/test/java/org/jsoup/integration/servlets/Deflateservlet.java
@@ -2,7 +2,6 @@ package org.jsoup.integration.servlets;
 
 import org.jsoup.integration.TestServer;
 
-import javax.servlet.ServletException;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import java.io.IOException;
diff --git a/src/test/java/org/jsoup/integration/servlets/FileServlet.java b/src/test/java/org/jsoup/integration/servlets/FileServlet.java
index 399c2e7a..6249232e 100644
--- a/src/test/java/org/jsoup/integration/servlets/FileServlet.java
+++ b/src/test/java/org/jsoup/integration/servlets/FileServlet.java
@@ -3,7 +3,6 @@ package org.jsoup.integration.servlets;
 import org.jsoup.integration.ParseTest;
 import org.jsoup.integration.TestServer;
 
-import javax.servlet.ServletException;
 import javax.servlet.ServletOutputStream;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
diff --git a/src/test/java/org/jsoup/integration/servlets/HelloServlet.java b/src/test/java/org/jsoup/integration/servlets/HelloServlet.java
index e6800894..7a7924c7 100644
--- a/src/test/java/org/jsoup/integration/servlets/HelloServlet.java
+++ b/src/test/java/org/jsoup/integration/servlets/HelloServlet.java
@@ -2,7 +2,6 @@ package org.jsoup.integration.servlets;
 
 import org.jsoup.integration.TestServer;
 
-import javax.servlet.ServletException;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import java.io.IOException;
diff --git a/src/test/java/org/jsoup/integration/servlets/RedirectServlet.java b/src/test/java/org/jsoup/integration/servlets/RedirectServlet.java
index 20084c49..5ab52a0b 100644
--- a/src/test/java/org/jsoup/integration/servlets/RedirectServlet.java
+++ b/src/test/java/org/jsoup/integration/servlets/RedirectServlet.java
@@ -2,11 +2,9 @@ package org.jsoup.integration.servlets;
 
 import org.jsoup.integration.TestServer;
 
-import javax.servlet.ServletException;
 import javax.servlet.http.Cookie;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
-import java.io.IOException;
 
 public class RedirectServlet extends BaseServlet {
     public static final String Url = TestServer.map(RedirectServlet.class);
@@ -36,7 +34,7 @@ public class RedirectServlet extends BaseServlet {
     }
 
     @Override
-    protected void doPost(HttpServletRequest req, HttpServletResponse res) throws IOException {
+    protected void doPost(HttpServletRequest req, HttpServletResponse res) {
         doGet(req, res);
     }
 }
diff --git a/src/test/java/org/jsoup/internal/ConstrainableInputStreamTest.java b/src/test/java/org/jsoup/internal/ConstrainableInputStreamTest.java
index fb84d9e1..6a7ad961 100644
--- a/src/test/java/org/jsoup/internal/ConstrainableInputStreamTest.java
+++ b/src/test/java/org/jsoup/internal/ConstrainableInputStreamTest.java
@@ -1,18 +1,17 @@
 package org.jsoup.internal;
 
 import org.jsoup.Jsoup;
-import org.junit.Ignore;
-import org.junit.Test;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
 
 import java.io.BufferedInputStream;
 import java.io.IOException;
 import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
+import static org.junit.jupiter.api.Assertions.*;
 
-@Ignore
+@Disabled
 public class ConstrainableInputStreamTest {
     // todo - move these all to local jetty, don't ignore
 
@@ -33,7 +32,7 @@ public class ConstrainableInputStreamTest {
         ByteBuffer firstBytes = stream.readToByteBuffer(bufferSize);
 
         byte[] array = firstBytes.array();
-        String firstText = new String(array, "UTF-8");
+        String firstText = new String(array, StandardCharsets.UTF_8);
         assertTrue(firstText.startsWith("<html><head><title>Large"));
         assertEquals(bufferSize, array.length);
 
@@ -45,7 +44,7 @@ public class ConstrainableInputStreamTest {
         ByteBuffer fullRead = stream.readToByteBuffer(0);
         byte[] fullArray = fullRead.array();
         assertEquals(capSize, fullArray.length);
-        String fullText = new String(fullArray, "UTF-8");
+        String fullText = new String(fullArray, StandardCharsets.UTF_8);
         assertTrue(fullText.startsWith(firstText));
     }
 
@@ -63,7 +62,7 @@ public class ConstrainableInputStreamTest {
         stream.mark(bufferSize);
         ByteBuffer firstBytes = stream.readToByteBuffer(bufferSize);
         byte[] array = firstBytes.array();
-        String firstText = new String(array, "UTF-8");
+        String firstText = new String(array, StandardCharsets.UTF_8);
         assertTrue(firstText.startsWith("<html><head><title>Large"));
         assertEquals(bufferSize, array.length);
 
@@ -72,7 +71,7 @@ public class ConstrainableInputStreamTest {
         ByteBuffer fullRead = stream.readToByteBuffer(0);
         byte[] fullArray = fullRead.array();
         assertEquals(280735, fullArray.length);
-        String fullText = new String(fullArray, "UTF-8");
+        String fullText = new String(fullArray, StandardCharsets.UTF_8);
         assertTrue(fullText.startsWith(firstText));
 
     }
diff --git a/src/test/java/org/jsoup/internal/StringUtilTest.java b/src/test/java/org/jsoup/internal/StringUtilTest.java
index ca39f60e..c674b97e 100644
--- a/src/test/java/org/jsoup/internal/StringUtilTest.java
+++ b/src/test/java/org/jsoup/internal/StringUtilTest.java
@@ -1,20 +1,19 @@
 package org.jsoup.internal;
 
 import org.jsoup.Jsoup;
-import org.jsoup.internal.StringUtil;
-import org.junit.Test;
+import org.junit.jupiter.api.Test;
 
 import java.util.Arrays;
 import java.util.Collections;
 
-import static org.jsoup.internal.StringUtil.*;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
+import static org.jsoup.internal.StringUtil.normaliseWhitespace;
+import static org.jsoup.internal.StringUtil.resolve;
+import static org.junit.jupiter.api.Assertions.*;
 
 public class StringUtilTest {
 
-    @Test public void join() {
+    @Test
+    public void join() {
         assertEquals("", StringUtil.join(Collections.singletonList(""), " "));
         assertEquals("one", StringUtil.join(Collections.singletonList("one"), " "));
         assertEquals("one two three", StringUtil.join(Arrays.asList("one", "two", "three"), " "));
diff --git a/src/test/java/org/jsoup/nodes/AttributeTest.java b/src/test/java/org/jsoup/nodes/AttributeTest.java
index c8edfd09..0ee16185 100644
--- a/src/test/java/org/jsoup/nodes/AttributeTest.java
+++ b/src/test/java/org/jsoup/nodes/AttributeTest.java
@@ -1,12 +1,13 @@
 package org.jsoup.nodes;
 
 import org.jsoup.Jsoup;
-import org.junit.Test;
+import org.junit.jupiter.api.Test;
 
-import static org.junit.Assert.*;
+import static org.junit.jupiter.api.Assertions.*;
 
 public class AttributeTest {
-    @Test public void html() {
+    @Test
+    public void html() {
         Attribute attr = new Attribute("key", "value &");
         assertEquals("key=\"value &amp;\"", attr.html());
         assertEquals(attr.html(), attr.toString());
@@ -19,13 +20,15 @@ public class AttributeTest {
         assertEquals(attr.html(), attr.toString());
     }
 
-    @Test(expected = IllegalArgumentException.class) public void validatesKeysNotEmpty() {
-        Attribute attr = new Attribute(" ", "Check");
+    @Test public void validatesKeysNotEmpty() {
+        assertThrows(IllegalArgumentException.class, () -> new Attribute(" ", "Check"));
     }
 
-    @Test(expected = IllegalArgumentException.class) public void validatesKeysNotEmptyViaSet() {
-        Attribute attr = new Attribute("One", "Check");
-        attr.setKey(" ");
+    @Test public void validatesKeysNotEmptyViaSet() {
+        assertThrows(IllegalArgumentException.class, () -> {
+            Attribute attr = new Attribute("One", "Check");
+            attr.setKey(" ");
+        });
     }
 
     @Test public void booleanAttributesAreEmptyStringValues() {
diff --git a/src/test/java/org/jsoup/nodes/AttributesTest.java b/src/test/java/org/jsoup/nodes/AttributesTest.java
index 14872553..84d9ed78 100644
--- a/src/test/java/org/jsoup/nodes/AttributesTest.java
+++ b/src/test/java/org/jsoup/nodes/AttributesTest.java
@@ -1,14 +1,12 @@
 package org.jsoup.nodes;
 
-import org.junit.Test;
+import org.junit.jupiter.api.Test;
 
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
+import static org.junit.jupiter.api.Assertions.*;
 
 /**
  * Tests for Attributes.
@@ -199,8 +197,8 @@ public class AttributesTest {
         for(Attribute at : a) {
             at.setKey("b");
         }
-        assertFalse("Attribute 'a' not correctly removed", a.hasKey("a"));
-        assertTrue("Attribute 'b' not present after renaming", a.hasKey("b"));
+        assertFalse(a.hasKey("a"), "Attribute 'a' not correctly removed");
+        assertTrue(a.hasKey("b"), "Attribute 'b' not present after renaming");
     }
 
     @Test
diff --git a/src/test/java/org/jsoup/nodes/BuildEntities.java b/src/test/java/org/jsoup/nodes/BuildEntities.java
index 58bb1fce..423c2680 100644
--- a/src/test/java/org/jsoup/nodes/BuildEntities.java
+++ b/src/test/java/org/jsoup/nodes/BuildEntities.java
@@ -11,7 +11,6 @@ import java.io.FileWriter;
 import java.io.IOException;
 import java.nio.file.Files;
 import java.util.ArrayList;
-import java.util.Collections;
 import java.util.Comparator;
 import java.util.Map;
 
@@ -49,14 +48,14 @@ class BuildEntities {
             }
             ref.name = name;
         }
-        Collections.sort(base, byName);
-        Collections.sort(full, byName);
+        base.sort(byName);
+        full.sort(byName);
 
         // now determine code point order
         ArrayList<CharacterRef> baseByCode = new ArrayList<>(base);
         ArrayList<CharacterRef> fullByCode = new ArrayList<>(full);
-        Collections.sort(baseByCode, byCode);
-        Collections.sort(fullByCode, byCode);
+        baseByCode.sort(byCode);
+        fullByCode.sort(byCode);
 
         // and update their codepoint index.
         @SuppressWarnings("unchecked") ArrayList<CharacterRef>[] codelists = new ArrayList[]{baseByCode, fullByCode};
diff --git a/src/test/java/org/jsoup/nodes/CommentTest.java b/src/test/java/org/jsoup/nodes/CommentTest.java
index 59d99ffc..fe1c1361 100644
--- a/src/test/java/org/jsoup/nodes/CommentTest.java
+++ b/src/test/java/org/jsoup/nodes/CommentTest.java
@@ -1,9 +1,9 @@
 package org.jsoup.nodes;
 
 import org.jsoup.Jsoup;
-import org.junit.Test;
+import org.junit.jupiter.api.Test;
 
-import static org.junit.Assert.*;
+import static org.junit.jupiter.api.Assertions.*;
 
 public class CommentTest {
     private Comment comment = new Comment(" This is one heck of a comment! ");
diff --git a/src/test/java/org/jsoup/nodes/DocumentTest.java b/src/test/java/org/jsoup/nodes/DocumentTest.java
index e6eab622..47177ea4 100644
--- a/src/test/java/org/jsoup/nodes/DocumentTest.java
+++ b/src/test/java/org/jsoup/nodes/DocumentTest.java
@@ -6,18 +6,14 @@ import org.jsoup.integration.ParseTest;
 import org.jsoup.nodes.Document.OutputSettings;
 import org.jsoup.nodes.Document.OutputSettings.Syntax;
 import org.jsoup.select.Elements;
-import org.junit.Ignore;
-import org.junit.Test;
-
-import java.io.ByteArrayInputStream;
-import java.io.File;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.StringWriter;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+
+import java.io.*;
 import java.nio.charset.Charset;
 import java.nio.charset.StandardCharsets;
 
-import static org.junit.Assert.*;
+import static org.junit.jupiter.api.Assertions.*;
 
 /**
  Tests for Document.
@@ -153,7 +149,7 @@ public class DocumentTest {
     }
 
     // Ignored since this test can take awhile to run.
-    @Ignore
+    @Disabled
     @Test public void testOverflowClone() {
         StringBuilder builder = new StringBuilder();
         for (int i = 0; i < 100000; i++) {
@@ -419,16 +415,16 @@ public class DocumentTest {
                         +     "before&nbsp;after"
                         +   "</body>"
                         + "</html>";
-        InputStream is = new ByteArrayInputStream(input.getBytes(Charset.forName("ASCII")));
+        InputStream is = new ByteArrayInputStream(input.getBytes(StandardCharsets.US_ASCII));
 
         Document doc = Jsoup.parse(is, null, "http://example.com");
         doc.outputSettings().escapeMode(Entities.EscapeMode.xhtml);
 
         String output = new String(doc.html().getBytes(doc.outputSettings().charset()), doc.outputSettings().charset());
 
-        assertFalse("Should not have contained a '?'.", output.contains("?"));
-        assertTrue("Should have contained a '&#xa0;' or a '&nbsp;'.",
-                output.contains("&#xa0;") || output.contains("&nbsp;"));
+        assertFalse(output.contains("?"), "Should not have contained a '?'.");
+        assertTrue(output.contains("&#xa0;") || output.contains("&nbsp;"),
+                "Should have contained a '&#xa0;' or a '&nbsp;'.");
     }
 
     @Test public void parseAndHtmlOnDifferentThreads() throws InterruptedException {
@@ -440,12 +436,9 @@ public class DocumentTest {
         final Elements p = doc.select("p");
         assertEquals(html, p.outerHtml());
 
-        Thread thread = new Thread(new Runnable() {
-            @Override
-            public void run() {
-                out[0] = p.outerHtml();
-                doc.outputSettings().charset(StandardCharsets.US_ASCII);
-            }
+        Thread thread = new Thread(() -> {
+            out[0] = p.outerHtml();
+            doc.outputSettings().charset(StandardCharsets.US_ASCII);
         });
         thread.start();
         thread.join();
diff --git a/src/test/java/org/jsoup/nodes/DocumentTypeTest.java b/src/test/java/org/jsoup/nodes/DocumentTypeTest.java
index 443feb75..afa5ae7f 100644
--- a/src/test/java/org/jsoup/nodes/DocumentTypeTest.java
+++ b/src/test/java/org/jsoup/nodes/DocumentTypeTest.java
@@ -2,9 +2,10 @@ package org.jsoup.nodes;
 
 import org.jsoup.Jsoup;
 import org.jsoup.parser.Parser;
-import org.junit.Test;
+import org.junit.jupiter.api.Test;
 
-import static org.junit.Assert.*;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
 
 /**
  * Tests for the DocumentType node
@@ -14,17 +15,17 @@ import static org.junit.Assert.*;
 public class DocumentTypeTest {
     @Test
     public void constructorValidationOkWithBlankName() {
-        DocumentType fail = new DocumentType("","", "");
+        new DocumentType("","", "");
     }
 
-    @Test(expected = IllegalArgumentException.class)
+    @Test
     public void constructorValidationThrowsExceptionOnNulls() {
-        DocumentType fail = new DocumentType("html", null, null);
+        assertThrows(IllegalArgumentException.class, () -> new DocumentType("html", null, null));
     }
 
     @Test
     public void constructorValidationOkWithBlankPublicAndSystemIds() {
-        DocumentType fail = new DocumentType("html","", "");
+        new DocumentType("html","", "");
     }
 
     @Test public void outerHtmlGeneration() {
diff --git a/src/test/java/org/jsoup/nodes/ElementIT.java b/src/test/java/org/jsoup/nodes/ElementIT.java
index bbd022c8..6b0add64 100644
--- a/src/test/java/org/jsoup/nodes/ElementIT.java
+++ b/src/test/java/org/jsoup/nodes/ElementIT.java
@@ -1,12 +1,12 @@
 package org.jsoup.nodes;
 
 import org.jsoup.Jsoup;
-import org.junit.Test;
+import org.junit.jupiter.api.Test;
 
 import java.util.List;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 
 public class ElementIT {
     @Test
diff --git a/src/test/java/org/jsoup/nodes/ElementTest.java b/src/test/java/org/jsoup/nodes/ElementTest.java
index 88a41297..74b3cb61 100644
--- a/src/test/java/org/jsoup/nodes/ElementTest.java
+++ b/src/test/java/org/jsoup/nodes/ElementTest.java
@@ -3,23 +3,13 @@ package org.jsoup.nodes;
 import org.jsoup.Jsoup;
 import org.jsoup.TextUtil;
 import org.jsoup.parser.Tag;
-import org.jsoup.select.Elements;
-import org.jsoup.select.Evaluator;
-import org.jsoup.select.NodeFilter;
-import org.jsoup.select.NodeVisitor;
-import org.jsoup.select.QueryParser;
-import org.junit.Test;
-
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.HashSet;
-import java.util.LinkedHashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
+import org.jsoup.select.*;
+import org.junit.jupiter.api.Test;
+
+import java.util.*;
 import java.util.concurrent.atomic.AtomicInteger;
 
-import static org.junit.Assert.*;
+import static org.junit.jupiter.api.Assertions.*;
 
 /**
  * Tests for Element (DOM stuff mostly).
@@ -444,7 +434,7 @@ public class ElementTest {
         assertEquals("", div.attr("true"));
 
         List<Attribute> attributes = div.attributes().asList();
-        assertEquals("There should be one attribute", 1, attributes.size());
+        assertEquals(1, attributes.size(), "There should be one attribute");
         assertFalse(div.hasAttr("false"));
 
         assertEquals("<div true></div>", div.outerHtml());
@@ -495,16 +485,20 @@ public class ElementTest {
         assertEquals("there &amp; now &gt; <p>Hello</p>", TextUtil.stripNewlines(div.html()));
     }
 
-    @Test(expected = IllegalArgumentException.class) public void testThrowsOnAddNullText() {
-        Document doc = Jsoup.parse("<div id=1><p>Hello</p></div>");
-        Element div = doc.getElementById("1");
-        div.appendText(null);
+    @Test public void testThrowsOnAddNullText() {
+        assertThrows(IllegalArgumentException.class, () -> {
+            Document doc = Jsoup.parse("<div id=1><p>Hello</p></div>");
+            Element div = doc.getElementById("1");
+            div.appendText(null);
+        });
     }
 
-    @Test(expected = IllegalArgumentException.class)  public void testThrowsOnPrependNullText() {
-        Document doc = Jsoup.parse("<div id=1><p>Hello</p></div>");
-        Element div = doc.getElementById("1");
-        div.prependText(null);
+    @Test public void testThrowsOnPrependNullText() {
+        assertThrows(IllegalArgumentException.class, () -> {
+            Document doc = Jsoup.parse("<div id=1><p>Hello</p></div>");
+            Element div = doc.getElementById("1");
+            div.prependText(null);
+        });
     }
 
     @Test public void testAddNewHtml() {
diff --git a/src/test/java/org/jsoup/nodes/EntitiesTest.java b/src/test/java/org/jsoup/nodes/EntitiesTest.java
index 05ed5fbc..a853f212 100644
--- a/src/test/java/org/jsoup/nodes/EntitiesTest.java
+++ b/src/test/java/org/jsoup/nodes/EntitiesTest.java
@@ -1,11 +1,11 @@
 package org.jsoup.nodes;
 
 import org.jsoup.Jsoup;
-import org.junit.Test;
+import org.junit.jupiter.api.Test;
 
 import static org.jsoup.nodes.Document.OutputSettings;
 import static org.jsoup.nodes.Entities.EscapeMode.*;
-import static org.junit.Assert.*;
+import static org.junit.jupiter.api.Assertions.assertEquals;
 
 public class EntitiesTest {
     @Test public void escape() {
diff --git a/src/test/java/org/jsoup/nodes/FormElementTest.java b/src/test/java/org/jsoup/nodes/FormElementTest.java
index bcb06030..a931efaf 100644
--- a/src/test/java/org/jsoup/nodes/FormElementTest.java
+++ b/src/test/java/org/jsoup/nodes/FormElementTest.java
@@ -2,11 +2,11 @@ package org.jsoup.nodes;
 
 import org.jsoup.Connection;
 import org.jsoup.Jsoup;
-import org.junit.Test;
+import org.junit.jupiter.api.Test;
 
 import java.util.List;
 
-import static org.junit.Assert.*;
+import static org.junit.jupiter.api.Assertions.*;
 
 /**
  * Tests for FormElement
@@ -90,7 +90,7 @@ public class FormElementTest {
 
         boolean threw = false;
         try {
-            Connection con = form.submit();
+            form.submit();
         } catch (IllegalArgumentException e) {
             threw = true;
             assertEquals("Could not determine a form action URL for submit. Ensure you set a base URI when parsing.",
diff --git a/src/test/java/org/jsoup/nodes/LeafNodeTest.java b/src/test/java/org/jsoup/nodes/LeafNodeTest.java
index 5978fba6..0398ebcf 100644
--- a/src/test/java/org/jsoup/nodes/LeafNodeTest.java
+++ b/src/test/java/org/jsoup/nodes/LeafNodeTest.java
@@ -3,9 +3,9 @@ package org.jsoup.nodes;
 import org.jsoup.Jsoup;
 import org.jsoup.select.Elements;
 import org.jsoup.select.NodeFilter;
-import org.junit.Test;
+import org.junit.jupiter.api.Test;
 
-import static org.junit.Assert.*;
+import static org.junit.jupiter.api.Assertions.*;
 
 public class LeafNodeTest {
 
diff --git a/src/test/java/org/jsoup/nodes/NodeTest.java b/src/test/java/org/jsoup/nodes/NodeTest.java
index 11bc796c..1dd66f11 100644
--- a/src/test/java/org/jsoup/nodes/NodeTest.java
+++ b/src/test/java/org/jsoup/nodes/NodeTest.java
@@ -4,11 +4,12 @@ import org.jsoup.Jsoup;
 import org.jsoup.TextUtil;
 import org.jsoup.parser.Tag;
 import org.jsoup.select.NodeVisitor;
-import org.junit.Test;
+import org.junit.jupiter.api.Test;
 
 import java.util.List;
 
-import static org.junit.Assert.*;
+import static org.junit.jupiter.api.Assertions.*;
+
 /**
  Tests Nodes
 
@@ -298,22 +299,22 @@ public class NodeTest {
 
         inputElement.attr("value","bar");
 
-        assertEquals(singletonAttributes("value", "bar"), getAttributesCaseInsensitive(inputElement, "value"));
+        assertEquals(singletonAttributes(), getAttributesCaseInsensitive(inputElement));
     }
 
-    private Attributes getAttributesCaseInsensitive(Element element, String attributeName) {
+    private Attributes getAttributesCaseInsensitive(Element element) {
         Attributes matches = new Attributes();
         for (Attribute attribute : element.attributes()) {
-            if (attribute.getKey().equalsIgnoreCase(attributeName)) {
+            if (attribute.getKey().equalsIgnoreCase("value")) {
                 matches.put(attribute);
             }
         }
         return matches;
     }
 
-    private Attributes singletonAttributes(String key, String value) {
+    private Attributes singletonAttributes() {
         Attributes attributes = new Attributes();
-        attributes.put(key, value);
+        attributes.put("value", "bar");
         return attributes;
     }
 }
diff --git a/src/test/java/org/jsoup/nodes/TextNodeTest.java b/src/test/java/org/jsoup/nodes/TextNodeTest.java
index 535bfbcb..18b78735 100644
--- a/src/test/java/org/jsoup/nodes/TextNodeTest.java
+++ b/src/test/java/org/jsoup/nodes/TextNodeTest.java
@@ -3,11 +3,11 @@ package org.jsoup.nodes;
 import org.jsoup.Jsoup;
 import org.jsoup.TextUtil;
 import org.jsoup.internal.StringUtil;
-import org.junit.Test;
+import org.junit.jupiter.api.Test;
 
 import java.util.List;
 
-import static org.junit.Assert.*;
+import static org.junit.jupiter.api.Assertions.*;
 
 /**
  Test TextNodes
diff --git a/src/test/java/org/jsoup/parser/AttributeParseTest.java b/src/test/java/org/jsoup/parser/AttributeParseTest.java
index 4967c7be..d328b9fb 100644
--- a/src/test/java/org/jsoup/parser/AttributeParseTest.java
+++ b/src/test/java/org/jsoup/parser/AttributeParseTest.java
@@ -1,16 +1,16 @@
 package org.jsoup.parser;
 
-import java.util.List;
-
 import org.jsoup.Jsoup;
 import org.jsoup.nodes.Attribute;
 import org.jsoup.nodes.Attributes;
 import org.jsoup.nodes.Document;
 import org.jsoup.nodes.Element;
 import org.jsoup.select.Elements;
-import org.junit.Test;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
 
-import static org.junit.Assert.*;
+import static org.junit.jupiter.api.Assertions.*;
 
 /**
  Test suite for attribute parser.
@@ -82,7 +82,7 @@ public class AttributeParseTest {
         assertEquals("", el.attr("empty"));
 
         List<Attribute> attributes = el.attributes().asList();
-        assertEquals("There should be 3 attribute present", 3, attributes.size());
+        assertEquals(3, attributes.size(), "There should be 3 attribute present");
 
         assertEquals(html, el.outerHtml()); // vets boolean syntax
     }
@@ -90,7 +90,7 @@ public class AttributeParseTest {
     @Test public void dropsSlashFromAttributeName() {
         String html = "<img /onerror='doMyJob'/>";
         Document doc = Jsoup.parse(html);
-        assertFalse("SelfClosingStartTag ignores last character", doc.select("img[onerror]").isEmpty());
+        assertFalse(doc.select("img[onerror]").isEmpty(), "SelfClosingStartTag ignores last character");
         assertEquals("<img onerror=\"doMyJob\">", doc.body().html());
 
         doc = Jsoup.parse(html, "", Parser.xmlParser());
diff --git a/src/test/java/org/jsoup/parser/CharacterReaderTest.java b/src/test/java/org/jsoup/parser/CharacterReaderTest.java
index 633aa4f9..ba34d47a 100644
--- a/src/test/java/org/jsoup/parser/CharacterReaderTest.java
+++ b/src/test/java/org/jsoup/parser/CharacterReaderTest.java
@@ -1,11 +1,11 @@
 package org.jsoup.parser;
 
-import org.junit.Test;
+import org.junit.jupiter.api.Test;
 
 import java.io.BufferedReader;
 import java.io.StringReader;
 
-import static org.junit.Assert.*;
+import static org.junit.jupiter.api.Assertions.*;
 
 /**
  * Test suite for character reader.
@@ -295,7 +295,7 @@ public class CharacterReaderTest {
     public void notEmptyAtBufferSplitPoint() {
         CharacterReader r = new CharacterReader(new StringReader("How about now"), 3);
         assertEquals("How", r.consumeTo(' '));
-        assertFalse("Should not be empty", r.isEmpty());
+        assertFalse(r.isEmpty(), "Should not be empty");
 
         assertEquals(' ', r.consume());
         assertFalse(r.isEmpty());
diff --git a/src/test/java/org/jsoup/parser/HtmlParserTest.java b/src/test/java/org/jsoup/parser/HtmlParserTest.java
index 6e830254..000ff978 100644
--- a/src/test/java/org/jsoup/parser/HtmlParserTest.java
+++ b/src/test/java/org/jsoup/parser/HtmlParserTest.java
@@ -2,21 +2,13 @@ package org.jsoup.parser;
 
 import org.jsoup.Jsoup;
 import org.jsoup.TextUtil;
-import org.jsoup.internal.StringUtil;
 import org.jsoup.integration.ParseTest;
-import org.jsoup.nodes.CDataNode;
-import org.jsoup.nodes.Comment;
-import org.jsoup.nodes.DataNode;
-import org.jsoup.nodes.Document;
-import org.jsoup.nodes.Element;
-import org.jsoup.nodes.Entities;
-import org.jsoup.nodes.FormElement;
-import org.jsoup.nodes.Node;
-import org.jsoup.nodes.TextNode;
+import org.jsoup.internal.StringUtil;
+import org.jsoup.nodes.*;
 import org.jsoup.safety.Whitelist;
 import org.jsoup.select.Elements;
-import org.junit.Ignore;
-import org.junit.Test;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
 
 import java.io.ByteArrayInputStream;
 import java.io.File;
@@ -24,7 +16,7 @@ import java.io.IOException;
 import java.util.List;
 
 import static org.jsoup.parser.ParseSettings.preserveCase;
-import static org.junit.Assert.*;
+import static org.junit.jupiter.api.Assertions.*;
 
 /**
  * Tests for the Parser
@@ -634,7 +626,7 @@ public class HtmlParserTest {
         assertEquals("<b>1</b>\n<p><b>2</b>3</p>", doc.body().html());
     }
 
-    @Ignore // todo: test case for https://github.com/jhy/jsoup/issues/845. Doesn't work yet.
+    @Disabled // todo: test case for https://github.com/jhy/jsoup/issues/845. Doesn't work yet.
     @Test public void handlesMisnestedAInDivs() {
         String h = "<a href='#1'><div><div><a href='#2'>child</a</div</div></a>";
         String w = "<a href=\"#1\"></a><div><a href=\"#1\"></a><div><a href=\"#1\"></a><a href=\"#2\">child</a></div></div>";
@@ -915,7 +907,7 @@ public class HtmlParserTest {
         Document doc = Jsoup.parse(html);
         Element el = doc.select("form").first();
 
-        assertTrue("Is form element", el instanceof FormElement);
+        assertTrue(el instanceof FormElement, "Is form element");
         FormElement form = (FormElement) el;
         Elements controls = form.elements();
         assertEquals(2, controls.size());
@@ -929,7 +921,7 @@ public class HtmlParserTest {
         Document doc = Jsoup.parse(html);
         Element el = doc.select("form").first();
 
-        assertTrue("Is form element", el instanceof FormElement);
+        assertTrue(el instanceof FormElement, "Is form element");
         FormElement form = (FormElement) el;
         Elements controls = form.elements();
         assertEquals(2, controls.size());
@@ -1000,9 +992,9 @@ public class HtmlParserTest {
         String rendered = doc.toString();
         int endOfEmail = rendered.indexOf("Comment");
         int guarantee = rendered.indexOf("Why am I here?");
-        assertTrue("Comment not found", endOfEmail > -1);
-        assertTrue("Search text not found", guarantee > -1);
-        assertTrue("Search text did not come after comment", guarantee > endOfEmail);
+        assertTrue(endOfEmail > -1, "Comment not found");
+        assertTrue(guarantee > -1, "Search text not found");
+        assertTrue(guarantee > endOfEmail, "Search text did not come after comment");
     }
 
     @Test public void testNormalisesIsIndex() {
diff --git a/src/test/java/org/jsoup/parser/HtmlTreeBuilderStateTest.java b/src/test/java/org/jsoup/parser/HtmlTreeBuilderStateTest.java
index 1836e409..97bd5287 100644
--- a/src/test/java/org/jsoup/parser/HtmlTreeBuilderStateTest.java
+++ b/src/test/java/org/jsoup/parser/HtmlTreeBuilderStateTest.java
@@ -1,7 +1,7 @@
 package org.jsoup.parser;
 
 import org.jsoup.parser.HtmlTreeBuilderState.Constants;
-import org.junit.Test;
+import org.junit.jupiter.api.Test;
 
 import java.lang.reflect.Field;
 import java.lang.reflect.Modifier;
@@ -9,14 +9,13 @@ import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
 
-import static org.junit.Assert.assertArrayEquals;
-import static org.junit.Assert.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
 
 public class HtmlTreeBuilderStateTest {
-    static List<Object[]> findArrays(Class<?> constantClass) {
+    static List<Object[]> findArrays() {
         ArrayList<Object[]> array = new ArrayList<>();
-
-        Field[] fields = constantClass.getDeclaredFields();
+        Field[] fields = Constants.class.getDeclaredFields();
 
         for (Field field : fields) {
             if (Modifier.isStatic(field.getModifiers()) && field.getType().isArray()) {
@@ -41,7 +40,7 @@ public class HtmlTreeBuilderStateTest {
 
     @Test
     public void ensureArraysAreSorted() {
-        List<Object[]> constants = findArrays(Constants.class);
+        List<Object[]> constants = findArrays();
         ensureSorted(constants);
         assertEquals(38, constants.size());
     }
diff --git a/src/test/java/org/jsoup/parser/HtmlTreeBuilderTest.java b/src/test/java/org/jsoup/parser/HtmlTreeBuilderTest.java
index 37533768..04630dc9 100644
--- a/src/test/java/org/jsoup/parser/HtmlTreeBuilderTest.java
+++ b/src/test/java/org/jsoup/parser/HtmlTreeBuilderTest.java
@@ -1,10 +1,11 @@
 package org.jsoup.parser;
 
-import org.junit.Test;
+
+import org.junit.jupiter.api.Test;
 
 import java.util.Arrays;
 
-import static org.junit.Assert.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
 
 public class HtmlTreeBuilderTest {
     @Test
diff --git a/src/test/java/org/jsoup/parser/ParserIT.java b/src/test/java/org/jsoup/parser/ParserIT.java
index a4c5173f..e5dee55b 100644
--- a/src/test/java/org/jsoup/parser/ParserIT.java
+++ b/src/test/java/org/jsoup/parser/ParserIT.java
@@ -1,10 +1,10 @@
 package org.jsoup.parser;
 
 import org.jsoup.nodes.Document;
-import org.junit.Test;
+import org.junit.jupiter.api.Test;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 
 /**
  * Longer running Parser tests.
diff --git a/src/test/java/org/jsoup/parser/ParserSettingsTest.java b/src/test/java/org/jsoup/parser/ParserSettingsTest.java
index a4903d6b..7856287c 100644
--- a/src/test/java/org/jsoup/parser/ParserSettingsTest.java
+++ b/src/test/java/org/jsoup/parser/ParserSettingsTest.java
@@ -1,17 +1,17 @@
 package org.jsoup.parser;
 
-import org.jsoup.MultiLocaleRule;
-import org.jsoup.MultiLocaleRule.MultiLocaleTest;
+import org.jsoup.MultiLocaleExtension.MultiLocaleTest;
 import org.jsoup.nodes.Attributes;
-import org.junit.Rule;
-import org.junit.Test;
 
-import static org.junit.Assert.assertEquals;
+import java.util.Locale;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
 
 public class ParserSettingsTest {
-    @Rule public MultiLocaleRule rule = new MultiLocaleRule();
+    @MultiLocaleTest
+    public void caseSupport(Locale locale) {
+        Locale.setDefault(locale);
 
-    @Test @MultiLocaleTest public void caseSupport() {
         ParseSettings bothOn = new ParseSettings(true, true);
         ParseSettings bothOff = new ParseSettings(false, false);
         ParseSettings tagOn = new ParseSettings(true, false);
@@ -30,15 +30,20 @@ public class ParserSettingsTest {
         assertEquals("ID", attrOn.normalizeAttribute("ID"));
     }
 
-    @Test @MultiLocaleTest public void attributeCaseNormalization() {
-        ParseSettings parseSettings = new ParseSettings(false, false);
+    @MultiLocaleTest
+    public void attributeCaseNormalization(Locale locale) {
+        Locale.setDefault(locale);
 
+        ParseSettings parseSettings = new ParseSettings(false, false);
         String normalizedAttribute = parseSettings.normalizeAttribute("HIDDEN");
 
         assertEquals("hidden", normalizedAttribute);
     }
 
-    @Test @MultiLocaleTest public void attributesCaseNormalization() {
+    @MultiLocaleTest
+    public void attributesCaseNormalization(Locale locale) {
+        Locale.setDefault(locale);
+
         ParseSettings parseSettings = new ParseSettings(false, false);
         Attributes attributes = new Attributes();
         attributes.put("ITEM", "1");
diff --git a/src/test/java/org/jsoup/parser/ParserTest.java b/src/test/java/org/jsoup/parser/ParserTest.java
index 6ab9e703..f77b2306 100644
--- a/src/test/java/org/jsoup/parser/ParserTest.java
+++ b/src/test/java/org/jsoup/parser/ParserTest.java
@@ -1,8 +1,8 @@
 package org.jsoup.parser;
 
-import org.junit.Test;
+import org.junit.jupiter.api.Test;
 
-import static org.junit.Assert.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
 
 public class ParserTest {
 
diff --git a/src/test/java/org/jsoup/parser/TagTest.java b/src/test/java/org/jsoup/parser/TagTest.java
index d05751ac..65a794d8 100644
--- a/src/test/java/org/jsoup/parser/TagTest.java
+++ b/src/test/java/org/jsoup/parser/TagTest.java
@@ -1,25 +1,26 @@
 package org.jsoup.parser;
 
-import org.jsoup.MultiLocaleRule;
-import org.jsoup.MultiLocaleRule.MultiLocaleTest;
-import org.junit.Rule;
-import org.junit.Test;
+import org.jsoup.MultiLocaleExtension.MultiLocaleTest;
+import org.junit.jupiter.api.Test;
 
-import static org.junit.Assert.*;
+import java.util.Locale;
+
+import static org.junit.jupiter.api.Assertions.*;
 
 /**
  Tag tests.
  @author Jonathan Hedley, jonathan@hedley.net */
 public class TagTest {
-    @Rule public MultiLocaleRule rule = new MultiLocaleRule();
-
     @Test public void isCaseSensitive() {
         Tag p1 = Tag.valueOf("P");
         Tag p2 = Tag.valueOf("p");
         assertNotEquals(p1, p2);
     }
 
-    @Test @MultiLocaleTest public void canBeInsensitive() {
+    @MultiLocaleTest
+    public void canBeInsensitive(Locale locale) {
+        Locale.setDefault(locale);
+
         Tag script1 = Tag.valueOf("script", ParseSettings.htmlDefault);
         Tag script2 = Tag.valueOf("SCRIPT", ParseSettings.htmlDefault);
         assertSame(script1, script2);
@@ -68,12 +69,12 @@ public class TagTest {
         assertTrue(foo.formatAsBlock());
     }
 
-    @Test(expected = IllegalArgumentException.class) public void valueOfChecksNotNull() {
-        Tag.valueOf(null);
+    @Test public void valueOfChecksNotNull() {
+        assertThrows(IllegalArgumentException.class, () -> Tag.valueOf(null));
     }
 
-    @Test(expected = IllegalArgumentException.class) public void valueOfChecksNotEmpty() {
-        Tag.valueOf(" ");
+    @Test public void valueOfChecksNotEmpty() {
+        assertThrows(IllegalArgumentException.class, () -> Tag.valueOf(" "));
     }
 
     @Test public void knownTags() {
diff --git a/src/test/java/org/jsoup/parser/TokenQueueTest.java b/src/test/java/org/jsoup/parser/TokenQueueTest.java
index 9f039a6a..2690c4bf 100644
--- a/src/test/java/org/jsoup/parser/TokenQueueTest.java
+++ b/src/test/java/org/jsoup/parser/TokenQueueTest.java
@@ -1,9 +1,10 @@
 package org.jsoup.parser;
 
 import org.jsoup.Jsoup;
-import org.junit.Test;
+import org.junit.jupiter.api.Test;
 
-import static org.junit.Assert.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.fail;
 
 /**
  * Token queue tests.
@@ -89,7 +90,7 @@ public class TokenQueueTest {
             TokenQueue tq = new TokenQueue("unbalanced(something(or another)) else");
             tq.consumeTo("(");
             tq.chompBalanced('(', '+');
-            org.junit.Assert.fail("should have thrown IllegalArgumentException");
+            fail("should have thrown IllegalArgumentException");
         } catch (IllegalArgumentException expected) {
             assertEquals("Did not find balanced marker at 'something(or another)) else'", expected.getMessage());
         }
diff --git a/src/test/java/org/jsoup/parser/TokeniserStateTest.java b/src/test/java/org/jsoup/parser/TokeniserStateTest.java
index f1939109..d8a791f3 100644
--- a/src/test/java/org/jsoup/parser/TokeniserStateTest.java
+++ b/src/test/java/org/jsoup/parser/TokeniserStateTest.java
@@ -7,12 +7,12 @@ import org.jsoup.nodes.Document;
 import org.jsoup.nodes.Element;
 import org.jsoup.nodes.TextNode;
 import org.jsoup.select.Elements;
-import org.junit.Test;
+import org.junit.jupiter.api.Test;
 
 import java.util.Arrays;
 
-import static org.junit.Assert.assertArrayEquals;
-import static org.junit.Assert.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
 
 public class TokeniserStateTest {
 
@@ -22,8 +22,6 @@ public class TokeniserStateTest {
     @Test
     public void ensureSearchArraysAreSorted() {
         char[][] arrays = {
-            TokeniserState.attributeSingleValueCharsSorted,
-            TokeniserState.attributeDoubleValueCharsSorted,
             TokeniserState.attributeNameCharsSorted,
             TokeniserState.attributeValueUnquoted
         };
@@ -143,7 +141,7 @@ public class TokeniserStateTest {
         String expectedOutput = "<!DOCTYPE html PUBLIC \"-//W3C//DTD HTML 4.0//EN\">";
         for (char q : quote) {
             for (char ws : whiteSpace) {
-                String[] htmls = { 
+                String[] htmls = {
                         String.format("<!DOCTYPE html%cPUBLIC %c-//W3C//DTD HTML 4.0//EN%c>", ws, q, q),
                         String.format("<!DOCTYPE html %cPUBLIC %c-//W3C//DTD HTML 4.0//EN%c>", ws, q, q),
                         String.format("<!DOCTYPE html PUBLIC%c%c-//W3C//DTD HTML 4.0//EN%c>", ws, q, q),
@@ -212,7 +210,7 @@ public class TokeniserStateTest {
         String triggeringSnippet = "<a href=\"\"foo";
         char[] padding = new char[CharacterReader.readAheadLimit - triggeringSnippet.length() + 2]; // The "foo" part must be just at the limit.
         Arrays.fill(padding, ' ');
-        String paddedSnippet = new StringBuilder().append(padding).append(triggeringSnippet).toString();
+        String paddedSnippet = String.valueOf(padding) + triggeringSnippet;
         ParseErrorList errorList = ParseErrorList.tracking(1);
 
         Parser.parseFragment(paddedSnippet, null, "", errorList);
diff --git a/src/test/java/org/jsoup/parser/TokeniserTest.java b/src/test/java/org/jsoup/parser/TokeniserTest.java
index ca80e308..d7a6b109 100644
--- a/src/test/java/org/jsoup/parser/TokeniserTest.java
+++ b/src/test/java/org/jsoup/parser/TokeniserTest.java
@@ -1,23 +1,15 @@
 package org.jsoup.parser;
 
-import static org.jsoup.parser.CharacterReader.maxBufferLen;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertTrue;
+import org.jsoup.Jsoup;
+import org.jsoup.nodes.*;
+import org.jsoup.select.Elements;
+import org.junit.jupiter.api.Test;
 
 import java.io.UnsupportedEncodingException;
 import java.util.Arrays;
 
-import org.jsoup.Jsoup;
-import org.jsoup.nodes.Attribute;
-import org.jsoup.nodes.CDataNode;
-import org.jsoup.nodes.Comment;
-import org.jsoup.nodes.Document;
-import org.jsoup.nodes.Element;
-import org.jsoup.nodes.Node;
-import org.jsoup.nodes.TextNode;
-import org.jsoup.select.Elements;
-import org.junit.Test;
+import static org.jsoup.parser.CharacterReader.maxBufferLen;
+import static org.junit.jupiter.api.Assertions.*;
 
 public class TokeniserTest {
     @Test
@@ -43,7 +35,7 @@ public class TokeniserTest {
             Document doc = Jsoup.parse(html);
             String src = doc.select("img").attr("src");
 
-            assertTrue("Handles for quote " + quote, src.contains("X"));
+            assertTrue(src.contains("X"), "Handles for quote " + quote);
             assertTrue(src.contains(tail));
         }
     }
@@ -181,7 +173,7 @@ public class TokeniserTest {
             // some of these characters are illegal
             if (s.charAt(0) == '\ufffd') { continue; }
 
-            assertEquals("At: " + i, s.charAt(0), Tokeniser.win1252Extensions[i]);
+            assertEquals(s.charAt(0), Tokeniser.win1252Extensions[i], "At: " + i);
         }
     }
 
@@ -197,7 +189,7 @@ public class TokeniserTest {
         Document doc = parser.parseInput(testMarkup, "");
 
         Node commentNode = doc.body().childNode(0);
-        assertTrue("Expected comment node", commentNode instanceof Comment);
+        assertTrue(commentNode instanceof Comment, "Expected comment node");
         assertEquals(expectedCommentData, ((Comment)commentNode).getData());
     }
 
@@ -214,7 +206,7 @@ public class TokeniserTest {
         Document doc = parser.parseInput(testMarkup, "");
 
         Node cdataNode = doc.body().childNode(0);
-        assertTrue("Expected CDATA node", cdataNode instanceof CDataNode);
+        assertTrue(cdataNode instanceof CDataNode, "Expected CDATA node");
         assertEquals(cdataContents, ((CDataNode)cdataNode).text());
     }
 }
diff --git a/src/test/java/org/jsoup/parser/XmlTreeBuilderTest.java b/src/test/java/org/jsoup/parser/XmlTreeBuilderTest.java
index 5dd18d85..8aa7547a 100644
--- a/src/test/java/org/jsoup/parser/XmlTreeBuilderTest.java
+++ b/src/test/java/org/jsoup/parser/XmlTreeBuilderTest.java
@@ -3,27 +3,21 @@ package org.jsoup.parser;
 import org.jsoup.Jsoup;
 import org.jsoup.TextUtil;
 import org.jsoup.internal.StringUtil;
-import org.jsoup.nodes.CDataNode;
-import org.jsoup.nodes.Document;
-import org.jsoup.nodes.Element;
-import org.jsoup.nodes.Node;
-import org.jsoup.nodes.TextNode;
-import org.jsoup.nodes.XmlDeclaration;
+import org.jsoup.nodes.*;
 import org.jsoup.select.Elements;
-import org.junit.Ignore;
-import org.junit.Test;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
 
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.net.URISyntaxException;
-import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
 import java.util.List;
 
 import static org.jsoup.nodes.Document.OutputSettings.Syntax;
-import static org.junit.Assert.*;
-import static org.junit.Assert.assertNull;
+import static org.junit.jupiter.api.Assertions.*;
 
 /**
  * Tests XmlTreeBuilder.
@@ -68,7 +62,7 @@ public class XmlTreeBuilderTest {
                 TextUtil.stripNewlines(doc.html()));
     }
 
-    @Ignore
+    @Disabled
     @Test
     public void testSupplyParserToConnection() throws IOException {
         String xmlUrl = "http://direct.infohound.net/tools/jsoup-xml-test.xml";
@@ -170,7 +164,7 @@ public class XmlTreeBuilderTest {
     public void testCreatesValidProlog() {
         Document document = Document.createShell("");
         document.outputSettings().syntax(Syntax.xml);
-        document.charset(Charset.forName("utf-8"));
+        document.charset(StandardCharsets.UTF_8);
         assertEquals("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
             "<html>\n" +
             " <head></head>\n" +
diff --git a/src/test/java/org/jsoup/safety/CleanerTest.java b/src/test/java/org/jsoup/safety/CleanerTest.java
index 26ad2888..707313f8 100644
--- a/src/test/java/org/jsoup/safety/CleanerTest.java
+++ b/src/test/java/org/jsoup/safety/CleanerTest.java
@@ -1,30 +1,28 @@
 package org.jsoup.safety;
 
-import org.jsoup.MultiLocaleRule;
-import org.jsoup.MultiLocaleRule.MultiLocaleTest;
 import org.jsoup.Jsoup;
+import org.jsoup.MultiLocaleExtension.MultiLocaleTest;
 import org.jsoup.TextUtil;
 import org.jsoup.nodes.Document;
 import org.jsoup.nodes.Entities;
-import org.junit.Rule;
-import org.junit.Test;
+import org.junit.jupiter.api.Test;
 
-import static org.junit.Assert.*;
+import java.util.Locale;
+
+import static org.junit.jupiter.api.Assertions.*;
 
 /**
  Tests for the cleaner.
 
  @author Jonathan Hedley, jonathan@hedley.net */
 public class CleanerTest {
-    @Rule public MultiLocaleRule rule = new MultiLocaleRule();
-
     @Test public void simpleBehaviourTest() {
         String h = "<div><p class=foo><a href='http://evil.com'>Hello <b id=bar>there</b>!</a></div>";
         String cleanHtml = Jsoup.clean(h, Whitelist.simpleText());
 
         assertEquals("Hello <b>there</b>!", TextUtil.stripNewlines(cleanHtml));
     }
-    
+
     @Test public void simpleBehaviourTest2() {
         String h = "Hello <b>there</b>!";
         String cleanHtml = Jsoup.clean(h, Whitelist.simpleText());
@@ -39,13 +37,13 @@ public class CleanerTest {
         assertEquals("<p><a rel=\"nofollow\">Dodgy</a> <a href=\"http://nice.com\" rel=\"nofollow\">Nice</a></p><blockquote>Hello</blockquote>",
                 TextUtil.stripNewlines(cleanHtml));
     }
-    
+
     @Test public void basicWithImagesTest() {
         String h = "<div><p><img src='http://example.com/' alt=Image></p><p><img src='ftp://ftp.example.com'></p></div>";
         String cleanHtml = Jsoup.clean(h, Whitelist.basicWithImages());
         assertEquals("<p><img src=\"http://example.com/\" alt=\"Image\"></p><p><img></p>", TextUtil.stripNewlines(cleanHtml));
     }
-    
+
     @Test public void testRelaxed() {
         String h = "<h1>Head</h1><table><tr><td>One<td>Two</td></tr></table>";
         String cleanHtml = Jsoup.clean(h, Whitelist.relaxed());
@@ -82,7 +80,10 @@ public class CleanerTest {
                 TextUtil.stripNewlines(cleanHtml));
     }
 
-    @Test @MultiLocaleTest public void whitelistedProtocolShouldBeRetained() {
+    @MultiLocaleTest
+    public void whitelistedProtocolShouldBeRetained(Locale locale) {
+        Locale.setDefault(locale);
+
         Whitelist whitelist = Whitelist.none()
                 .addTags("a")
                 .addAttributes("a", "href")
@@ -98,25 +99,25 @@ public class CleanerTest {
         String cleanHtml = Jsoup.clean(h, Whitelist.relaxed());
         assertEquals("<p>Hello</p>", cleanHtml);
     }
-    
+
     @Test public void testDropXmlProc() {
         String h = "<?import namespace=\"xss\"><p>Hello</p>";
         String cleanHtml = Jsoup.clean(h, Whitelist.relaxed());
         assertEquals("<p>Hello</p>", cleanHtml);
     }
-    
+
     @Test public void testDropScript() {
         String h = "<SCRIPT SRC=//ha.ckers.org/.j><SCRIPT>alert(/XSS/.source)</SCRIPT>";
         String cleanHtml = Jsoup.clean(h, Whitelist.relaxed());
         assertEquals("", cleanHtml);
     }
-    
+
     @Test public void testDropImageScript() {
         String h = "<IMG SRC=\"javascript:alert('XSS')\">";
         String cleanHtml = Jsoup.clean(h, Whitelist.relaxed());
         assertEquals("<img>", cleanHtml);
     }
-    
+
     @Test public void testCleanJavascriptHref() {
         String h = "<A HREF=\"javascript:document.location='http://www.google.com/'\">XSS</A>";
         String cleanHtml = Jsoup.clean(h, Whitelist.relaxed());
@@ -150,7 +151,7 @@ public class CleanerTest {
         String cleanHtml = Jsoup.clean(h, Whitelist.relaxed());
         assertEquals("<p>Test</p>", cleanHtml);
     }
-    
+
     @Test public void testHandlesEmptyAttributes() {
         String h = "<img alt=\"\" src= unknown=''>";
         String cleanHtml = Jsoup.clean(h, Whitelist.basicWithImages());
@@ -190,7 +191,7 @@ public class CleanerTest {
         assertFalse(cleaner.isValid(Jsoup.parse(nok)));
         assertFalse(new Cleaner(Whitelist.none()).isValid(okDoc));
     }
-    
+
     @Test public void resolvesRelativeLinks() {
         String html = "<a href='/foo'>Link</a><img src='/bar'>";
         String clean = Jsoup.clean(html, "http://example.com/", Whitelist.basicWithImages());
@@ -202,7 +203,7 @@ public class CleanerTest {
         String clean = Jsoup.clean(html, "http://example.com/", Whitelist.basicWithImages().preserveRelativeLinks(true));
         assertEquals("<a href=\"/foo\" rel=\"nofollow\">Link</a>\n<img src=\"/bar\"> \n<img>", clean);
     }
-    
+
     @Test public void dropsUnresolvableRelativeLinks() {
         String html = "<a href='/foo'>Link</a>";
         String clean = Jsoup.clean(html, Whitelist.basic());
@@ -283,14 +284,16 @@ public class CleanerTest {
         assertTrue( Jsoup.isValid("Hello<script>alert('Doh')</script>World !", whitelist ) );
     }
 
-    @Test(expected = IllegalArgumentException.class)
+    @Test
     public void bailsIfRemovingProtocolThatsNotSet() {
-        // a case that came up on the email list
-        Whitelist w = Whitelist.none();
-
-        // note no add tag, and removing protocol without adding first
-        w.addAttributes("a", "href");
-        w.removeProtocols("a", "href", "javascript"); // with no protocols enforced, this was a noop. Now validates.
+        assertThrows(IllegalArgumentException.class, () -> {
+            // a case that came up on the email list
+            Whitelist w = Whitelist.none();
+
+            // note no add tag, and removing protocol without adding first
+            w.addAttributes("a", "href");
+            w.removeProtocols("a", "href", "javascript"); // with no protocols enforced, this was a noop. Now validates.
+        });
     }
 
     @Test public void handlesControlCharactersAfterTagName() {
diff --git a/src/test/java/org/jsoup/select/CssTest.java b/src/test/java/org/jsoup/select/CssTest.java
index 746e8d36..956d7f12 100644
--- a/src/test/java/org/jsoup/select/CssTest.java
+++ b/src/test/java/org/jsoup/select/CssTest.java
@@ -1,23 +1,24 @@
 package org.jsoup.select;
 
-import static org.junit.Assert.*;
-
 import org.jsoup.Jsoup;
 import org.jsoup.nodes.Document;
 import org.jsoup.parser.Tag;
-import org.junit.Before;
-import org.junit.BeforeClass;
-import org.junit.Test;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.*;
+
 
 public class CssTest {
 
 	private Document html = null;
 	private static String htmlString;
-	
-	@BeforeClass
+
+	@BeforeAll
 	public static void initClass() {
 		StringBuilder sb = new StringBuilder("<html><head></head><body>");
-		
+
 		sb.append("<div id='pseudo'>");
 		for (int i = 1; i <= 10; i++) {
 			sb.append(String.format("<p>%d</p>",i));
@@ -35,20 +36,20 @@ public class CssTest {
 
 		sb.append("<span id='onlySpan'><br /></span>");
 		sb.append("<p class='empty'><!-- Comment only is still empty! --></p>");
-		
+
 		sb.append("<div id='only'>");
 		sb.append("Some text before the <em>only</em> child in this div");
 		sb.append("</div>");
-		
+
 		sb.append("</body></html>");
 		htmlString = sb.toString();
 	}
 
-	@Before
+	@BeforeEach
 	public void init() {
 		html  = Jsoup.parse(htmlString);
 	}
-	
+
 	@Test
 	public void firstChild() {
 		check(html.select("#pseudo :first-child"), "1");
@@ -60,7 +61,7 @@ public class CssTest {
 		check(html.select("#pseudo :last-child"), "10");
 		check(html.select("html:last-child"));
 	}
-	
+
 	@Test
 	public void nthChild_simple() {
 		for(int i = 1; i <=10; i++) {
@@ -88,7 +89,7 @@ public class CssTest {
 			check(html.select(String.format("#type p:nth-of-type(%d)", i)), String.valueOf(i));
 		}
 	}
-	
+
 	@Test
 	public void nthLastOfType_simple() {
 		for(int i = 1; i <=10; i++) {
@@ -124,7 +125,7 @@ public class CssTest {
 		check(html.select("#type :nth-of-type(+5)"), "5", "5", "5", "5");
 	}
 
-	
+
 	@Test
 	public void nthLastChild_advanced() {
 		check(html.select("#pseudo :nth-last-child(-5)"));
@@ -154,7 +155,7 @@ public class CssTest {
 		check(html.select("#type span:nth-last-of-type(-2n+5)"), "6", "8", "10");
 		check(html.select("#type :nth-last-of-type(+5)"), "6", "6", "6", "6");
 	}
-	
+
 	@Test
 	public void firstOfType() {
 		check(html.select("div:not(#only) :first-of-type"), "1", "1", "1", "1", "1");
@@ -173,16 +174,16 @@ public class CssTest {
 		assertEquals("br", sel.get(1).tagName());
 		assertEquals("p", sel.get(2).tagName());
 	}
-	
+
 	@Test
 	public void onlyChild() {
 		final Elements sel = html.select("span :only-child");
 		assertEquals(1, sel.size());
 		assertEquals("br", sel.get(0).tagName());
-		
+
 		check(html.select("#only :only-child"), "only");
 	}
-	
+
 	@Test
 	public void onlyOfType() {
 		final Elements sel = html.select(":only-of-type");
@@ -195,16 +196,15 @@ public class CssTest {
 		assertTrue(sel.get(4).hasClass("empty"));
 		assertEquals("em", sel.get(5).tagName());
 	}
-	
+
 	protected void check(Elements result, String...expectedContent ) {
-		assertEquals("Number of elements", expectedContent.length, result.size());
+		assertEquals(expectedContent.length, result.size(), "Number of elements");
 		for (int i = 0; i < expectedContent.length; i++) {
 			assertNotNull(result.get(i));
-			assertEquals("Expected element",expectedContent[i], result.get(i).ownText());
+			assertEquals(expectedContent[i], result.get(i).ownText(), "Expected element");
 		}
 	}
 
-	
 	@Test
 	public void root() {
 		Elements sel = html.select(":root");
diff --git a/src/test/java/org/jsoup/select/ElementsTest.java b/src/test/java/org/jsoup/select/ElementsTest.java
index e7244df9..c41a01b9 100644
--- a/src/test/java/org/jsoup/select/ElementsTest.java
+++ b/src/test/java/org/jsoup/select/ElementsTest.java
@@ -2,18 +2,12 @@ package org.jsoup.select;
 
 import org.jsoup.Jsoup;
 import org.jsoup.TextUtil;
-import org.jsoup.nodes.Comment;
-import org.jsoup.nodes.DataNode;
-import org.jsoup.nodes.Document;
-import org.jsoup.nodes.Element;
-import org.jsoup.nodes.FormElement;
-import org.jsoup.nodes.Node;
-import org.jsoup.nodes.TextNode;
-import org.junit.Test;
+import org.jsoup.nodes.*;
+import org.junit.jupiter.api.Test;
 
 import java.util.List;
 
-import static org.junit.Assert.*;
+import static org.junit.jupiter.api.Assertions.*;
 
 /**
  Tests for ElementList.
diff --git a/src/test/java/org/jsoup/select/QueryParserTest.java b/src/test/java/org/jsoup/select/QueryParserTest.java
index f2ac60cf..95c4da16 100644
--- a/src/test/java/org/jsoup/select/QueryParserTest.java
+++ b/src/test/java/org/jsoup/select/QueryParserTest.java
@@ -1,7 +1,8 @@
 package org.jsoup.select;
 
-import org.junit.Test;
-import static org.junit.Assert.*;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.*;
 
 /**
  * Tests for the Selector Query Parser.
@@ -40,23 +41,21 @@ public class QueryParserTest {
         assertEquals(2, andLeft.evaluators.size());
     }
 
-    @Test(expected = Selector.SelectorParseException.class) public void exceptionOnUncloseAttribute() {
-        Evaluator parse = QueryParser.parse("section > a[href=\"]");
+    @Test public void exceptionOnUncloseAttribute() {
+        assertThrows(Selector.SelectorParseException.class, () -> QueryParser.parse("section > a[href=\"]"));
     }
 
-    @Test(expected = Selector.SelectorParseException.class)  public void testParsesSingleQuoteInContains() {
-        Evaluator parse = QueryParser.parse("p:contains(One \" One)");
+    @Test public void testParsesSingleQuoteInContains() {
+        assertThrows(Selector.SelectorParseException.class, () -> QueryParser.parse("p:contains(One \" One)"));
     }
 
 
-    @Test(expected = Selector.SelectorParseException.class)
-    public void exceptOnEmptySelector() {
-        Evaluator parse = QueryParser.parse("");
+    @Test public void exceptOnEmptySelector() {
+        assertThrows(Selector.SelectorParseException.class, () -> QueryParser.parse(""));
     }
 
-    @Test(expected = Selector.SelectorParseException.class)
-    public void exceptOnNullSelector() {
-        Evaluator parse = QueryParser.parse(null);
+    @Test public void exceptOnNullSelector() {
+        assertThrows(Selector.SelectorParseException.class, () -> QueryParser.parse(null));
     }
 
     @Test public void okOnSpacesForeAndAft() {
diff --git a/src/test/java/org/jsoup/select/SelectorTest.java b/src/test/java/org/jsoup/select/SelectorTest.java
index 7720b254..347f80cc 100644
--- a/src/test/java/org/jsoup/select/SelectorTest.java
+++ b/src/test/java/org/jsoup/select/SelectorTest.java
@@ -1,15 +1,15 @@
 package org.jsoup.select;
 
 import org.jsoup.Jsoup;
-import org.jsoup.MultiLocaleRule;
-import org.jsoup.MultiLocaleRule.MultiLocaleTest;
+import org.jsoup.MultiLocaleExtension.MultiLocaleTest;
 import org.jsoup.nodes.Document;
 import org.jsoup.nodes.Element;
 import org.jsoup.parser.Parser;
-import org.junit.Rule;
-import org.junit.Test;
+import org.junit.jupiter.api.Test;
 
-import static org.junit.Assert.*;
+import java.util.Locale;
+
+import static org.junit.jupiter.api.Assertions.*;
 
 /**
  * Tests that the selector selects correctly.
@@ -17,8 +17,6 @@ import static org.junit.Assert.*;
  * @author Jonathan Hedley, jonathan@hedley.net
  */
 public class SelectorTest {
-    @Rule public MultiLocaleRule rule = new MultiLocaleRule();
-
     @Test public void testByTag() {
         // should be case insensitive
         Elements els = Jsoup.parse("<div id=1><div id=2><p>Hello</p></div></div><DIV id=3>").select("DIV");
@@ -64,7 +62,11 @@ public class SelectorTest {
         assertEquals("Two", elsFromClass.get(1).text());
     }
 
-    @Test @MultiLocaleTest public void testByAttribute() {
+
+    @MultiLocaleTest
+    public void testByAttribute(Locale locale) {
+        Locale.setDefault(locale);
+
         String h = "<div Title=Foo /><div Title=Bar /><div Style=Qux /><div title=Balim /><div title=SLIM />" +
                 "<div data-name='with spaces'/>";
         Document doc = Jsoup.parse(h);
@@ -187,7 +189,10 @@ public class SelectorTest {
         assertEquals(1, els2.size());
     }
 
-    @Test @MultiLocaleTest public void testByAttributeStarting() {
+    @MultiLocaleTest
+    public void testByAttributeStarting(Locale locale) {
+        Locale.setDefault(locale);
+
         Document doc = Jsoup.parse("<div id=1 ATTRIBUTE data-name=jsoup>Hello</div><p data-val=5 id=2>There</p><p id=3>No</p>");
         Elements withData = doc.select("[^data-]");
         assertEquals(2, withData.size());
@@ -281,7 +286,7 @@ public class SelectorTest {
         String h = "<div class=head><p class=first>Hello</p><p>There</p></div><p>None</p>";
         Document doc = Jsoup.parse(h);
         Element root = doc.getElementsByClass("HEAD").first();
-        
+
         Elements els = root.select(".head p");
         assertEquals(2, els.size());
         assertEquals("Hello", els.get(0).text());
@@ -293,7 +298,7 @@ public class SelectorTest {
 
         Elements empty = root.select("p .first"); // self, not descend, should not match
         assertEquals(0, empty.size());
-        
+
         Elements aboveRoot = root.select("body div.head");
         assertEquals(0, aboveRoot.size());
     }
@@ -571,7 +576,10 @@ public class SelectorTest {
         assertEquals("Two", divs.first().text());
     }
 
-    @Test @MultiLocaleTest public void testPseudoContains() {
+    @MultiLocaleTest
+    public void testPseudoContains(Locale locale) {
+        Locale.setDefault(locale);
+
         Document doc = Jsoup.parse("<div><p>The Rain.</p> <p class=light>The <i>RAIN</i>.</p> <p>Rain, the.</p></div>");
 
         Elements ps1 = doc.select("p:contains(Rain)");
@@ -609,7 +617,10 @@ public class SelectorTest {
         assertEquals("2", ps2.first().id());
     }
 
-    @Test @MultiLocaleTest public void containsOwn() {
+    @MultiLocaleTest
+    public void containsOwn(Locale locale) {
+        Locale.setDefault(locale);
+
         Document doc = Jsoup.parse("<p id=1>Hello <b>there</b> igor</p>");
         Elements ps = doc.select("p:containsOwn(Hello IGOR)");
         assertEquals(1, ps.size());
@@ -721,24 +732,24 @@ public class SelectorTest {
         assertEquals("div", doc.select("div[k" + s + "]").first().tagName());
         assertEquals("div", doc.select("div:containsOwn(" + s + ")").first().tagName());
     }
-    
+
     @Test
     public void selectClassWithSpace() {
         final String html = "<div class=\"value\">class without space</div>\n"
                           + "<div class=\"value \">class with space</div>";
-        
+
         Document doc = Jsoup.parse(html);
-        
+
         Elements found = doc.select("div[class=value ]");
         assertEquals(2, found.size());
         assertEquals("class without space", found.get(0).text());
         assertEquals("class with space", found.get(1).text());
-        
+
         found = doc.select("div[class=\"value \"]");
         assertEquals(2, found.size());
         assertEquals("class without space", found.get(0).text());
         assertEquals("class with space", found.get(1).text());
-        
+
         found = doc.select("div[class=\"value\\ \"]");
         assertEquals(0, found.size());
     }
@@ -763,7 +774,10 @@ public class SelectorTest {
         assertEquals("Two", doc.select("div[data=\"[Another)]]\"]").first().text());
     }
 
-    @Test @MultiLocaleTest public void containsData() {
+    @MultiLocaleTest
+    public void containsData(Locale locale) {
+        Locale.setDefault(locale);
+
         String html = "<p>function</p><script>FUNCTION</script><style>item</style><span><!-- comments --></span>";
         Document doc = Jsoup.parse(html);
         Element body = doc.body();
diff --git a/src/test/java/org/jsoup/select/TraversorTest.java b/src/test/java/org/jsoup/select/TraversorTest.java
index c8af3648..00f70d32 100644
--- a/src/test/java/org/jsoup/select/TraversorTest.java
+++ b/src/test/java/org/jsoup/select/TraversorTest.java
@@ -1,11 +1,11 @@
 package org.jsoup.select;
 
-import static org.junit.Assert.assertEquals;
-
 import org.jsoup.Jsoup;
 import org.jsoup.nodes.Document;
 import org.jsoup.nodes.Node;
-import org.junit.Test;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
 
 public class TraversorTest {
     // Note: NodeTraversor.traverse(new NodeVisitor) is tested in