Repository: deepjavalibrary/djl
Pull request #1336 (2021-11-01 06:18:02)

[api] Fix jar model url download bug
fixes #1333

Change-Id: Iee96a8e9fd1104ba4865db6b94709dba5882a371

## Description ##

Brief description of what this PR is about

- If this change is a backward incompatible change, why must this change be made?
- Interesting edge cases to note here


codecov-commenter: # [Codecov](https://codecov.io/gh/deepjavalibrary/djl/pull/1336?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=deepjavalibrary) Report
> Merging [#1336](https://codecov.io/gh/deepjavalibrary/djl/pull/1336?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=deepjavalibrary) (9f14338) into [master](https://codecov.io/gh/deepjavalibrary/djl/commit/bb5073f7ffc33f1883621fc1783dbb01e70b418f?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=deepjavalibrary) (bb5073f) will **decrease** coverage by `0.46%`.
> The diff coverage is `55.59%`.

[![Impacted file tree graph](https://codecov.io/gh/deepjavalibrary/djl/pull/1336/graphs/tree.svg?width=650&height=150&src=pr&token=mDKVNwFXRV&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=deepjavalibrary)](https://codecov.io/gh/deepjavalibrary/djl/pull/1336?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=deepjavalibrary)

```diff
@@             Coverage Diff              @@
##             master    #1336      +/-   ##
============================================
- Coverage     72.08%   71.62%   -0.47%     
- Complexity     5126     5145      +19     
============================================
  Files           473      478       +5     
  Lines         21970    22257     +287     
  Branches       2351     2385      +34     
============================================
+ Hits          15838    15941     +103     
- Misses         4925     5109     +184     
  Partials       1207     1207              
```


| [Impacted Files](https://codecov.io/gh/deepjavalibrary/djl/pull/1336?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=deepjavalibrary) | Coverage Δ | |
|---|---|---|
| [...i/djl/modality/cv/translator/BigGANTranslator.java](https://codecov.io/gh/deepjavalibrary/djl/pull/1336/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=deepjavalibrary#diff-YXBpL3NyYy9tYWluL2phdmEvYWkvZGpsL21vZGFsaXR5L2N2L3RyYW5zbGF0b3IvQmlnR0FOVHJhbnNsYXRvci5qYXZh) | `21.42% <ø> (-5.24%)` | :arrow_down: |
| [...odality/cv/translator/BigGANTranslatorFactory.java](https://codecov.io/gh/deepjavalibrary/djl/pull/1336/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=deepjavalibrary#diff-YXBpL3NyYy9tYWluL2phdmEvYWkvZGpsL21vZGFsaXR5L2N2L3RyYW5zbGF0b3IvQmlnR0FOVHJhbnNsYXRvckZhY3RvcnkuamF2YQ==) | `33.33% <0.00%> (+8.33%)` | :arrow_up: |
| [...nslator/InstanceSegmentationTranslatorFactory.java](https://codecov.io/gh/deepjavalibrary/djl/pull/1336/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=deepjavalibrary#diff-YXBpL3NyYy9tYWluL2phdmEvYWkvZGpsL21vZGFsaXR5L2N2L3RyYW5zbGF0b3IvSW5zdGFuY2VTZWdtZW50YXRpb25UcmFuc2xhdG9yRmFjdG9yeS5qYXZh) | `14.28% <0.00%> (-3.90%)` | :arrow_down: |
| [.../modality/cv/translator/YoloTranslatorFactory.java](https://codecov.io/gh/deepjavalibrary/djl/pull/1336/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=deepjavalibrary#diff-YXBpL3NyYy9tYWluL2phdmEvYWkvZGpsL21vZGFsaXR5L2N2L3RyYW5zbGF0b3IvWW9sb1RyYW5zbGF0b3JGYWN0b3J5LmphdmE=) | `8.33% <0.00%> (-1.67%)` | :arrow_down: |
| [...i/djl/modality/cv/translator/YoloV5Translator.java](https://codecov.io/gh/deepjavalibrary/djl/pull/1336/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=deepjavalibrary#diff-YXBpL3NyYy9tYWluL2phdmEvYWkvZGpsL21vZGFsaXR5L2N2L3RyYW5zbGF0b3IvWW9sb1Y1VHJhbnNsYXRvci5qYXZh) | `5.69% <0.00%> (ø)` | |
| [...odality/cv/translator/YoloV5TranslatorFactory.java](https://codecov.io/gh/deepjavalibrary/djl/pull/1336/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=deepjavalibrary#diff-YXBpL3NyYy9tYWluL2phdmEvYWkvZGpsL21vZGFsaXR5L2N2L3RyYW5zbGF0b3IvWW9sb1Y1VHJhbnNsYXRvckZhY3RvcnkuamF2YQ==) | `8.33% <0.00%> (-1.67%)` | :arrow_down: |
| [...n/java/ai/djl/modality/nlp/bert/BertTokenizer.java](https://codecov.io/gh/deepjavalibrary/djl/pull/1336/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=deepjavalibrary#diff-YXBpL3NyYy9tYWluL2phdmEvYWkvZGpsL21vZGFsaXR5L25scC9iZXJ0L0JlcnRUb2tlbml6ZXIuamF2YQ==) | `92.68% <0.00%> (-2.32%)` | :arrow_down: |
| [...modality/nlp/embedding/TrainableWordEmbedding.java](https://codecov.io/gh/deepjavalibrary/djl/pull/1336/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=deepjavalibrary#diff-YXBpL3NyYy9tYWluL2phdmEvYWkvZGpsL21vZGFsaXR5L25scC9lbWJlZGRpbmcvVHJhaW5hYmxlV29yZEVtYmVkZGluZy5qYXZh) | `45.00% <0.00%> (-3.34%)` | :arrow_down: |
| [...pi/src/main/java/ai/djl/ndarray/BytesSupplier.java](https://codecov.io/gh/deepjavalibrary/djl/pull/1336/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=deepjavalibrary#diff-YXBpL3NyYy9tYWluL2phdmEvYWkvZGpsL25kYXJyYXkvQnl0ZXNTdXBwbGllci5qYXZh) | `54.54% <0.00%> (-12.13%)` | :arrow_down: |
| [...i/src/main/java/ai/djl/ndarray/NDArrayAdapter.java](https://codecov.io/gh/deepjavalibrary/djl/pull/1336/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=deepjavalibrary#diff-YXBpL3NyYy9tYWluL2phdmEvYWkvZGpsL25kYXJyYXkvTkRBcnJheUFkYXB0ZXIuamF2YQ==) | `15.23% <0.00%> (-0.83%)` | :arrow_down: |
| ... and [88 more](https://codecov.io/gh/deepjavalibrary/djl/pull/1336/diff?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=deepjavalibrary) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/deepjavalibrary/djl/pull/1336?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=deepjavalibrary).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=deepjavalibrary)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/deepjavalibrary/djl/pull/1336?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=deepjavalibrary). Last update [866161a...9f14338](https://codecov.io/gh/deepjavalibrary/djl/pull/1336?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=deepjavalibrary). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=deepjavalibrary).


Patch:
diff --git a/api/src/main/java/ai/djl/modality/Input.java b/api/src/main/java/ai/djl/modality/Input.java
index e9fdb28c9..472f53070 100644
--- a/api/src/main/java/ai/djl/modality/Input.java
+++ b/api/src/main/java/ai/djl/modality/Input.java
@@ -16,9 +16,11 @@ import ai.djl.ndarray.BytesSupplier;
 import ai.djl.ndarray.NDArray;
 import ai.djl.ndarray.NDList;
 import ai.djl.ndarray.NDManager;
+import ai.djl.util.Pair;
 import ai.djl.util.PairList;
 import java.nio.charset.StandardCharsets;
 import java.util.Map;
+import java.util.Map.Entry;
 import java.util.TreeMap;
 
 /** A class stores the generic input data for inference. */
@@ -324,4 +326,27 @@ public class Input {
         }
         return NDList.decode(manager, data.getAsBytes());
     }
+
+    /** {@inheritDoc} * */
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder(1000);
+        sb.append("Input:\n");
+        for (Entry<String, String> property : properties.entrySet()) {
+            sb.append("Property ")
+                    .append(property.getKey())
+                    .append(": ")
+                    .append(property.getValue())
+                    .append('\n');
+        }
+        for (Pair<String, BytesSupplier> c : content) {
+            sb.append("Content ")
+                    .append(c.getKey())
+                    .append(": ")
+                    .append(c.getValue().toString())
+                    .append('\n');
+        }
+        sb.append('\n');
+        return sb.toString();
+    }
 }
diff --git a/api/src/main/java/ai/djl/repository/JarRepository.java b/api/src/main/java/ai/djl/repository/JarRepository.java
index 481880fd2..2469e43e8 100644
--- a/api/src/main/java/ai/djl/repository/JarRepository.java
+++ b/api/src/main/java/ai/djl/repository/JarRepository.java
@@ -108,7 +108,7 @@ public class JarRepository extends AbstractRepository {
         artifact.getArguments().putAll(arguments);
         Map<String, Artifact.Item> files = new ConcurrentHashMap<>();
         Artifact.Item item = new Artifact.Item();
-        item.setUri(uri.getPath());
+        item.setUri(uri.getSchemeSpecificPart());
         item.setName(""); // avoid creating extra folder
         item.setArtifact(artifact);
         item.setSize(-1);
diff --git a/api/src/main/java/ai/djl/repository/RepositoryFactoryImpl.java b/api/src/main/java/ai/djl/repository/RepositoryFactoryImpl.java
index 404cdb21e..a1c0ad3ac 100644
--- a/api/src/main/java/ai/djl/repository/RepositoryFactoryImpl.java
+++ b/api/src/main/java/ai/djl/repository/RepositoryFactoryImpl.java
@@ -118,6 +118,10 @@ class RepositoryFactoryImpl implements RepositoryFactory {
         if (uriPath == null) {
             uriPath = uri.getSchemeSpecificPart();
         }
+        if (uriPath.startsWith("file:")) {
+            // handle jar:file:/ url
+            uriPath = uriPath.substring(5);
+        }
         if (uriPath.startsWith("/") && System.getProperty("os.name").startsWith("Win")) {
             uriPath = uriPath.substring(1);
         }
diff --git a/engines/dlr/dlr-engine/build.gradle b/engines/dlr/dlr-engine/build.gradle
index 90a4c8e49..7c3021620 100644
--- a/engines/dlr/dlr-engine/build.gradle
+++ b/engines/dlr/dlr-engine/build.gradle
@@ -3,6 +3,7 @@ group "ai.djl.dlr"
 dependencies {
     api project(":api")
 
+    testImplementation(project(":testing"))
     testImplementation("org.testng:testng:${testng_version}") {
         exclude group: "junit", module: "junit"
     }
diff --git a/engines/ml/xgboost/build.gradle b/engines/ml/xgboost/build.gradle
index 0373ba985..68864e214 100644
--- a/engines/ml/xgboost/build.gradle
+++ b/engines/ml/xgboost/build.gradle
@@ -26,6 +26,7 @@ dependencies {
 
     exclusion project(":api")
     exclusion "commons-logging:commons-logging:${commons_logging_version}"
+    testImplementation(project(":testing"))
     testImplementation("org.testng:testng:${testng_version}") {
         exclude group: "junit", module: "junit"
     }
diff --git a/engines/tflite/tflite-engine/build.gradle b/engines/tflite/tflite-engine/build.gradle
index de7685ba9..deac6478d 100644
--- a/engines/tflite/tflite-engine/build.gradle
+++ b/engines/tflite/tflite-engine/build.gradle
@@ -8,6 +8,7 @@ dependencies {
         exclude group: "junit", module: "junit"
     }
 
+    testImplementation(project(":testing"))
     testRuntimeOnly "org.slf4j:slf4j-simple:${slf4j_version}"
 }
 
diff --git a/examples/build.gradle b/examples/build.gradle
index fbd5dfed1..fa974152e 100644
--- a/examples/build.gradle
+++ b/examples/build.gradle
@@ -19,6 +19,7 @@ dependencies {
     testImplementation("org.testng:testng:${testng_version}") {
         exclude group: "junit", module: "junit"
     }
+    testImplementation(project(":testing"))
 }
 
 application {
diff --git a/examples/src/main/java/ai/djl/examples/inference/BigGAN.java b/examples/src/main/java/ai/djl/examples/inference/BigGAN.java
index 0f4c16f92..44132352d 100644
--- a/examples/src/main/java/ai/djl/examples/inference/BigGAN.java
+++ b/examples/src/main/java/ai/djl/examples/inference/BigGAN.java
@@ -36,14 +36,14 @@ public final class BigGAN {
     private BigGAN() {}
 
     public static void main(String[] args) throws ModelException, TranslateException, IOException {
-        Image[] generatedImages = BigGAN.generate();
-
-        if (generatedImages == null) {
+        if (!"PyTorch".equals(Engine.getInstance().getEngineName())) {
             logger.info("This example only works for PyTorch Engine");
-        } else {
-            logger.info("Using PyTorch Engine. {} images generated.", generatedImages.length);
-            saveImages(generatedImages);
+            return;
         }
+
+        Image[] generatedImages = BigGAN.generate();
+        logger.info("Using PyTorch Engine. {} images generated.", generatedImages.length);
+        saveImages(generatedImages);
     }
 
     private static void saveImages(Image[] generatedImages) throws IOException {
@@ -58,9 +58,6 @@ public final class BigGAN {
     }
 
     public static Image[] generate() throws IOException, ModelException, TranslateException {
-        if (!"PyTorch".equals(Engine.getInstance().getEngineName())) {
-            return null;
-        }
 
         Criteria<int[], Image[]> criteria =
                 Criteria.builder()
diff --git a/examples/src/main/java/ai/djl/examples/inference/ObjectDetectionWithTensorflowSavedModel.java b/examples/src/main/java/ai/djl/examples/inference/ObjectDetectionWithTensorflowSavedModel.java
index 66e1b4488..e0473e752 100644
--- a/examples/src/main/java/ai/djl/examples/inference/ObjectDetectionWithTensorflowSavedModel.java
+++ b/examples/src/main/java/ai/djl/examples/inference/ObjectDetectionWithTensorflowSavedModel.java
@@ -69,18 +69,16 @@ public final class ObjectDetectionWithTensorflowSavedModel {
     private ObjectDetectionWithTensorflowSavedModel() {}
 
     public static void main(String[] args) throws IOException, ModelException, TranslateException {
-        DetectedObjects detection = ObjectDetectionWithTensorflowSavedModel.predict();
-        if (detection == null) {
+        if (!"TensorFlow".equals(Engine.getInstance().getEngineName())) {
             logger.info("This example only works for TensorFlow Engine");
-        } else {
-            logger.info("{}", detection);
+            return;
         }
+
+        DetectedObjects detection = ObjectDetectionWithTensorflowSavedModel.predict();
+        logger.info("{}", detection);
     }
 
     public static DetectedObjects predict() throws IOException, ModelException, TranslateException {
-        if (!"TensorFlow".equals(Engine.getInstance().getEngineName())) {
-            return null;
-        }
 
         Path imageFile = Paths.get("src/test/resources/dog_bike_car.jpg");
         Image img = ImageFactory.getInstance().fromFile(imageFile);
diff --git a/examples/src/main/java/ai/djl/examples/inference/StyleTransfer.java b/examples/src/main/java/ai/djl/examples/inference/StyleTransfer.java
index 59a3283ad..e4e3679f9 100644
--- a/examples/src/main/java/ai/djl/examples/inference/StyleTransfer.java
+++ b/examples/src/main/java/ai/djl/examples/inference/StyleTransfer.java
@@ -46,17 +46,18 @@ public final class StyleTransfer {
     }
 
     public static void main(String[] args) throws IOException, ModelException, TranslateException {
+        if (!"PyTorch".equals(Engine.getInstance().getEngineName())) {
+            logger.info("This example only works for PyTorch Engine");
+            return;
+        }
+
         Artist artist = Artist.MONET;
         String imagePath = "src/test/resources/mountains.png";
         Image input = ImageFactory.getInstance().fromFile(Paths.get(imagePath));
         Image output = transfer(input, artist);
 
-        if (output == null) {
-            logger.info("This example only works for PyTorch Engine");
-        } else {
-            logger.info("Using PyTorch Engine. " + artist + " painting generated.");
-            save(output, artist.toString(), "build/output/cyclegan/");
-        }
+        logger.info("Using PyTorch Engine. " + artist + " painting generated.");
+        save(output, artist.toString(), "build/output/cyclegan/");
     }
 
     public static Image transfer(Image image, Artist artist)
diff --git a/examples/src/main/java/ai/djl/examples/inference/sr/SuperResolution.java b/examples/src/main/java/ai/djl/examples/inference/sr/SuperResolution.java
index 6b8a68cbe..713a30531 100644
--- a/examples/src/main/java/ai/djl/examples/inference/sr/SuperResolution.java
+++ b/examples/src/main/java/ai/djl/examples/inference/sr/SuperResolution.java
@@ -45,6 +45,10 @@ public final class SuperResolution {
     private SuperResolution() {}
 
     public static void main(String[] args) throws ModelException, TranslateException, IOException {
+        if (!"TensorFlow".equals(Engine.getInstance().getEngineName())) {
+            logger.info("This example only works for TensorFlow Engine");
+            return;
+        }
         String imagePath = "src/test/resources/";
         ImageFactory imageFactory = ImageFactory.getInstance();
 
@@ -53,12 +57,8 @@ public final class SuperResolution {
 
         List<Image> enhancedImages = enhance(inputImages);
 
-        if (enhancedImages == null) {
-            logger.info("This example only works for TensorFlow Engine");
-        } else {
-            logger.info("Using TensorFlow Engine. {} images generated.", enhancedImages.size());
-            saveImages(inputImages, enhancedImages);
-        }
+        logger.info("Using TensorFlow Engine. {} images generated.", enhancedImages.size());
+        saveImages(inputImages, enhancedImages);
     }
 
     private static void saveImages(List<Image> input, List<Image> generated) throws IOException {
@@ -106,10 +106,6 @@ public final class SuperResolution {
     public static List<Image> enhance(List<Image> inputImages)
             throws IOException, ModelException, TranslateException {
 
-        if (!"TensorFlow".equals(Engine.getInstance().getEngineName())) {
-            return null;
-        }
-
         String modelUrl =
                 "https://storage.googleapis.com/tfhub-modules/captain-pool/esrgan-tf2/1.tar.gz";
         Criteria<Image, Image> criteria =
diff --git a/extensions/benchmark/README.md b/extensions/benchmark/README.md
index 73a8af9e4..68e4f72b9 100644
--- a/extensions/benchmark/README.md
+++ b/extensions/benchmark/README.md
@@ -168,6 +168,8 @@ usage: djl-bench [-p MODEL-PATH] -s INPUT-SHAPES [OPTIONS]
     --model-arguments <MODEL-ARGUMENTS>   Specify model loading arguments.
     --model-options <MODEL-OPTIONS>       Specify model loading options.
  -n,--model-name <MODEL-NAME>             Specify model file name.
+    --neuron-cores <NEURON-CORES>         Number of neuron cores to run multithreading inference, See
+                                          https://awsdocs-neuron.readthedocs-hosted.com.
  -o,--output-dir <OUTPUT-DIR>             Directory for output logs.
  -p,--model-path <MODEL-PATH>             Model directory file path.
  -s,--input-shapes <INPUT-SHAPES>         Input data shapes for the model.
diff --git a/extensions/benchmark/src/main/java/ai/djl/benchmark/Arguments.java b/extensions/benchmark/src/main/java/ai/djl/benchmark/Arguments.java
index 46a8f0b53..a35c58719 100644
--- a/extensions/benchmark/src/main/java/ai/djl/benchmark/Arguments.java
+++ b/extensions/benchmark/src/main/java/ai/djl/benchmark/Arguments.java
@@ -47,6 +47,7 @@ public class Arguments {
     private int iteration;
     private int threads;
     private int maxGpus;
+    private int neuronCores;
     private int delay;
     private PairList<DataType, Shape> inputShapes;
 
@@ -94,6 +95,9 @@ public class Arguments {
         } else {
             maxGpus = Integer.MAX_VALUE;
         }
+        if (cmd.hasOption("neuron-cores")) {
+            neuronCores = Integer.parseInt(cmd.getOptionValue("neuron-cores"));
+        }
         if (cmd.hasOption("threads")) {
             threads = Integer.parseInt(cmd.getOptionValue("threads"));
             Engine eng = Engine.getEngine(engine);
@@ -204,13 +208,23 @@ public class Arguments {
                         .argName("NUMBER_THREADS")
                         .desc("Number of inference threads.")
                         .build());
-        options.addOption(
+        OptionGroup deviceGroup = new OptionGroup();
+        deviceGroup.addOption(
                 Option.builder("g")
                         .longOpt("gpus")
                         .hasArg()
                         .argName("NUMBER_GPUS")
                         .desc("Number of GPUS to run multithreading inference.")
                         .build());
+        deviceGroup.addOption(
+                Option.builder()
+                        .longOpt("neuron-cores")
+                        .hasArg()
+                        .argName("NEURON-CORES")
+                        .desc(
+                                "Number of neuron cores to run multithreading inference, See https://awsdocs-neuron.readthedocs-hosted.com.")
+                        .build());
+        options.addOptionGroup(deviceGroup);
         options.addOption(
                 Option.builder("l")
                         .longOpt("delay")
@@ -302,6 +316,10 @@ public class Arguments {
         return maxGpus;
     }
 
+    int getNeuronCores() {
+        return neuronCores;
+    }
+
     String getOutputDir() {
         if (outputDir == null) {
             outputDir = "build";
diff --git a/extensions/benchmark/src/main/java/ai/djl/benchmark/MultithreadedBenchmark.java b/extensions/benchmark/src/main/java/ai/djl/benchmark/MultithreadedBenchmark.java
index a49ee749e..06a64af6f 100644
--- a/extensions/benchmark/src/main/java/ai/djl/benchmark/MultithreadedBenchmark.java
+++ b/extensions/benchmark/src/main/java/ai/djl/benchmark/MultithreadedBenchmark.java
@@ -22,6 +22,7 @@ import ai.djl.training.listener.MemoryTrainingListener;
 import ai.djl.translate.TranslateException;
 import java.io.IOException;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.List;
 import java.util.concurrent.Callable;
 import java.util.concurrent.ExecutionException;
@@ -47,6 +48,15 @@ public class MultithreadedBenchmark extends AbstractBenchmark {
         Engine engine = Engine.getEngine(arguments.getEngine());
         Device[] devices = engine.getDevices(arguments.getMaxGpus());
         int numOfThreads = arguments.getThreads();
+        int neuronCores = arguments.getNeuronCores();
+        if (neuronCores > 0) {
+            devices = new Device[neuronCores];
+            Arrays.fill(devices, Device.cpu());
+            if (numOfThreads > 1) {
+                numOfThreads = 2 * neuronCores;
+            }
+        }
+
         int delay = arguments.getDelay();
         AtomicInteger counter = new AtomicInteger(iteration);
         logger.info("Multithreading inference with {} threads.", numOfThreads);
diff --git a/extensions/fasttext/build.gradle b/extensions/fasttext/build.gradle
index 910b0d975..b3c16cfb4 100644
--- a/extensions/fasttext/build.gradle
+++ b/extensions/fasttext/build.gradle
@@ -10,6 +10,7 @@ dependencies {
 
     // ModelZoo requires at least one engine in the class path
     testImplementation project(":engines:pytorch:pytorch-engine")
+    testImplementation(project(":testing"))
     testImplementation "org.slf4j:slf4j-simple:${slf4j_version}"
 }
 
diff --git a/extensions/hadoop/build.gradle b/extensions/hadoop/build.gradle
index ce41c4668..2130fcf92 100644
--- a/extensions/hadoop/build.gradle
+++ b/extensions/hadoop/build.gradle
@@ -6,6 +6,7 @@ dependencies {
 
     testImplementation project(":engines:mxnet:mxnet-engine")
     testImplementation "org.apache.hadoop:hadoop-minicluster:${hadoop_version}"
+    testImplementation(project(":testing"))
 
     testImplementation("org.testng:testng:${testng_version}");
 }
diff --git a/extensions/sentencepiece/build.gradle b/extensions/sentencepiece/build.gradle
index 767c0444b..e46229873 100644
--- a/extensions/sentencepiece/build.gradle
+++ b/extensions/sentencepiece/build.gradle
@@ -8,6 +8,7 @@ dependencies {
     }
 
     testImplementation project(":engines:pytorch:pytorch-engine")
+    testImplementation(project(":testing"))
     testImplementation "org.slf4j:slf4j-simple:${slf4j_version}"
 }
 


Test patch:
diff --git a/api/src/test/java/ai/djl/repository/JarRepositoryTest.java b/api/src/test/java/ai/djl/repository/JarRepositoryTest.java
index 871f1ed34..920f5b357 100644
--- a/api/src/test/java/ai/djl/repository/JarRepositoryTest.java
+++ b/api/src/test/java/ai/djl/repository/JarRepositoryTest.java
@@ -15,6 +15,8 @@ package ai.djl.repository;
 import ai.djl.util.Utils;
 import ai.djl.util.ZipUtils;
 import java.io.IOException;
+import java.net.URL;
+import java.net.URLClassLoader;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
@@ -26,7 +28,7 @@ public class JarRepositoryTest {
 
     @Test
     public void testResource() throws IOException {
-        Path path = Paths.get("build/resources/test/");
+        Path path = Paths.get("build/tmp/");
         Files.createDirectories(path);
 
         Path dir = Paths.get("build/testDir/");
@@ -35,16 +37,24 @@ public class JarRepositoryTest {
         Files.createFile(dir.resolve("synset.txt"));
         Path testFile = path.resolve("test.zip");
         ZipUtils.zip(dir, testFile, false);
+        Path jarFile = path.resolve("test.jar");
+        ZipUtils.zip(testFile, jarFile, false);
 
-        Repository repo = Repository.newInstance("test", "jar:///test.zip");
-        Assert.assertEquals("test", repo.getName());
-        Assert.assertTrue(repo.isRemote());
+        URL[] url = {jarFile.toUri().toURL()};
+        try {
+            Thread.currentThread().setContextClassLoader(new URLClassLoader(url));
+            Repository repo = Repository.newInstance("test", "jar:///test.zip");
+            Assert.assertEquals("test", repo.getName());
+            Assert.assertTrue(repo.isRemote());
 
-        List<MRL> list = repo.getResources();
-        Assert.assertEquals(list.size(), 1);
+            List<MRL> list = repo.getResources();
+            Assert.assertEquals(list.size(), 1);
 
-        Artifact artifact = repo.resolve(list.get(0), null);
-        repo.prepare(artifact);
-        Assert.assertEquals(1, artifact.getFiles().size());
+            Artifact artifact = repo.resolve(list.get(0), null);
+            repo.prepare(artifact);
+            Assert.assertEquals(1, artifact.getFiles().size());
+        } finally {
+            Thread.currentThread().setContextClassLoader(null);
+        }
     }
 }
diff --git a/djl-zero/src/test/java/ai/djl/zero/cv/ImageClassificationTest.java b/djl-zero/src/test/java/ai/djl/zero/cv/ImageClassificationTest.java
index 14916b5a9..6389e17c0 100644
--- a/djl-zero/src/test/java/ai/djl/zero/cv/ImageClassificationTest.java
+++ b/djl-zero/src/test/java/ai/djl/zero/cv/ImageClassificationTest.java
@@ -16,13 +16,13 @@ import ai.djl.MalformedModelException;
 import ai.djl.modality.cv.Image;
 import ai.djl.repository.zoo.ModelNotFoundException;
 import ai.djl.repository.zoo.ZooModel;
+import ai.djl.testing.TestRequirements;
 import ai.djl.zero.Performance;
 import ai.djl.zero.cv.ImageClassification.Classes;
 import java.io.IOException;
 import java.io.InputStream;
 import java.net.URL;
 import java.nio.file.Path;
-import org.testng.SkipException;
 import org.testng.annotations.Test;
 
 public class ImageClassificationTest {
@@ -31,9 +31,8 @@ public class ImageClassificationTest {
     public void testImageClassificationPretrained()
             throws IOException, ModelNotFoundException, MalformedModelException {
         Class<?>[] inputClasses = {Image.class, Path.class, URL.class, InputStream.class};
-        if (!Boolean.getBoolean("nightly")) {
-            throw new SkipException("Nightly only");
-        }
+        TestRequirements.nightly();
+
         for (Class<?> inputClass : inputClasses) {
             for (Classes classes : Classes.values()) {
                 for (Performance performance : Performance.values()) {
diff --git a/engines/dlr/dlr-engine/src/test/java/ai/djl/dlr/engine/DlrNDManagerTest.java b/engines/dlr/dlr-engine/src/test/java/ai/djl/dlr/engine/DlrNDManagerTest.java
index 9154b00d8..a8a5ca727 100644
--- a/engines/dlr/dlr-engine/src/test/java/ai/djl/dlr/engine/DlrNDManagerTest.java
+++ b/engines/dlr/dlr-engine/src/test/java/ai/djl/dlr/engine/DlrNDManagerTest.java
@@ -15,18 +15,17 @@ package ai.djl.dlr.engine;
 import ai.djl.ndarray.NDArray;
 import ai.djl.ndarray.types.DataType;
 import ai.djl.ndarray.types.Shape;
+import ai.djl.testing.TestRequirements;
 import java.nio.ByteBuffer;
 import org.testng.Assert;
-import org.testng.SkipException;
 import org.testng.annotations.Test;
 
 public class DlrNDManagerTest {
 
     @Test
     public void testNDArray() {
-        if (System.getProperty("os.name").toLowerCase().startsWith("win")) {
-            throw new SkipException("test only work on mac and Linux");
-        }
+        TestRequirements.notWindows();
+
         try (DlrNDManager manager =
                 (DlrNDManager) DlrNDManager.getSystemManager().newSubManager()) {
             NDArray zeros = manager.zeros(new Shape(1, 2));
diff --git a/engines/dlr/dlr-engine/src/test/java/ai/djl/dlr/integration/DlrTest.java b/engines/dlr/dlr-engine/src/test/java/ai/djl/dlr/integration/DlrTest.java
index a60bda8d5..190deed84 100644
--- a/engines/dlr/dlr-engine/src/test/java/ai/djl/dlr/integration/DlrTest.java
+++ b/engines/dlr/dlr-engine/src/test/java/ai/djl/dlr/integration/DlrTest.java
@@ -23,26 +23,28 @@ import ai.djl.modality.cv.transform.ToTensor;
 import ai.djl.modality.cv.translator.ImageClassificationTranslator;
 import ai.djl.repository.zoo.Criteria;
 import ai.djl.repository.zoo.ZooModel;
+import ai.djl.testing.TestRequirements;
 import ai.djl.training.util.ProgressBar;
 import ai.djl.translate.TranslateException;
 import java.io.IOException;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import org.testng.Assert;
-import org.testng.SkipException;
 import org.testng.annotations.Test;
 
 public class DlrTest {
 
     @Test
     public void testDlr() throws ModelException, IOException, TranslateException {
+        TestRequirements.notWindows();
+
         String os;
         if (System.getProperty("os.name").toLowerCase().startsWith("mac")) {
             os = "osx";
         } else if (System.getProperty("os.name").toLowerCase().startsWith("linux")) {
             os = "linux";
         } else {
-            throw new SkipException("test only work on mac and Linux");
+            throw new IllegalStateException("Unexpected os");
         }
         ImageClassificationTranslator translator =
                 ImageClassificationTranslator.builder()
diff --git a/engines/ml/xgboost/src/test/java/ai/djl/ml/xgboost/XgbModelTest.java b/engines/ml/xgboost/src/test/java/ai/djl/ml/xgboost/XgbModelTest.java
index 02c97ed6e..97ccb3460 100644
--- a/engines/ml/xgboost/src/test/java/ai/djl/ml/xgboost/XgbModelTest.java
+++ b/engines/ml/xgboost/src/test/java/ai/djl/ml/xgboost/XgbModelTest.java
@@ -22,6 +22,7 @@ import ai.djl.ndarray.NDManager;
 import ai.djl.ndarray.types.DataType;
 import ai.djl.ndarray.types.Shape;
 import ai.djl.ndarray.types.SparseFormat;
+import ai.djl.testing.TestRequirements;
 import ai.djl.training.util.DownloadUtils;
 import ai.djl.translate.NoopTranslator;
 import ai.djl.translate.TranslateException;
@@ -31,7 +32,6 @@ import java.nio.FloatBuffer;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import org.testng.Assert;
-import org.testng.SkipException;
 import org.testng.annotations.BeforeClass;
 import org.testng.annotations.Test;
 
@@ -64,9 +64,8 @@ public class XgbModelTest {
 
     @Test
     public void testNDArray() {
-        if (System.getProperty("os.name").toLowerCase().startsWith("win")) {
-            throw new SkipException("test only work on mac and Linux");
-        }
+        TestRequirements.notWindows();
+
         try (XgbNDManager manager =
                 (XgbNDManager) XgbNDManager.getSystemManager().newSubManager()) {
             NDArray zeros = manager.zeros(new Shape(1, 2));
diff --git a/engines/mxnet/mxnet-model-zoo/src/test/java/ai/djl/mxnet/integration/MxSymbolBlockTest.java b/engines/mxnet/mxnet-model-zoo/src/test/java/ai/djl/mxnet/integration/MxSymbolBlockTest.java
index c4a882b8c..973281b6e 100644
--- a/engines/mxnet/mxnet-model-zoo/src/test/java/ai/djl/mxnet/integration/MxSymbolBlockTest.java
+++ b/engines/mxnet/mxnet-model-zoo/src/test/java/ai/djl/mxnet/integration/MxSymbolBlockTest.java
@@ -32,6 +32,7 @@ import ai.djl.repository.zoo.Criteria;
 import ai.djl.repository.zoo.ModelNotFoundException;
 import ai.djl.repository.zoo.ZooModel;
 import ai.djl.testing.Assertions;
+import ai.djl.testing.TestRequirements;
 import ai.djl.training.DefaultTrainingConfig;
 import ai.djl.training.GradientCollector;
 import ai.djl.training.ParameterStore;
@@ -47,7 +48,6 @@ import java.nio.file.Paths;
 import java.util.List;
 import java.util.stream.Collectors;
 import org.testng.Assert;
-import org.testng.SkipException;
 import org.testng.annotations.Test;
 
 public class MxSymbolBlockTest {
@@ -96,11 +96,12 @@ public class MxSymbolBlockTest {
     @Test
     public void trainWithNewParam()
             throws IOException, ModelNotFoundException, MalformedModelException {
-        if (Engine.getInstance().getGpuCount() == 0) {
+        if ("MXNet".equals(Engine.getInstance().getEngineName())) {
             // TODO: WARN The gradMeans (but not predictions or loss) changed during the upgrade
             // to MXNet 1.8. The issue affect only CPU, but GPU has not changed.
-            throw new SkipException("Ignore engine error");
+            TestRequirements.gpu();
         }
+
         TrainingConfig config =
                 new DefaultTrainingConfig(Loss.softmaxCrossEntropyLoss())
                         .optInitializer(Initializer.ONES, Parameter.Type.WEIGHT);
diff --git a/engines/tflite/tflite-engine/src/test/java/ai/djl/tflite/engine/TfLiteNDManagerTest.java b/engines/tflite/tflite-engine/src/test/java/ai/djl/tflite/engine/TfLiteNDManagerTest.java
index 7e2a7ae42..42ab68356 100644
--- a/engines/tflite/tflite-engine/src/test/java/ai/djl/tflite/engine/TfLiteNDManagerTest.java
+++ b/engines/tflite/tflite-engine/src/test/java/ai/djl/tflite/engine/TfLiteNDManagerTest.java
@@ -15,17 +15,16 @@ package ai.djl.tflite.engine;
 import ai.djl.ndarray.NDArray;
 import ai.djl.ndarray.NDManager;
 import ai.djl.ndarray.types.Shape;
+import ai.djl.testing.TestRequirements;
 import org.testng.Assert;
-import org.testng.SkipException;
 import org.testng.annotations.Test;
 
 public class TfLiteNDManagerTest {
 
     @Test
     public void testNDArray() {
-        if (System.getProperty("os.name").toLowerCase().startsWith("win")) {
-            throw new SkipException("test only work on mac and Linux");
-        }
+        TestRequirements.notWindows();
+
         try (NDManager manager = TfLiteNDManager.getSystemManager().newSubManager()) {
             NDArray zeros = manager.zeros(new Shape(1, 2));
             float[] data = zeros.toFloatArray();
diff --git a/engines/tflite/tflite-engine/src/test/java/ai/djl/tflite/engine/TfLiteTest.java b/engines/tflite/tflite-engine/src/test/java/ai/djl/tflite/engine/TfLiteTest.java
index cf2b6d814..8414be2b9 100644
--- a/engines/tflite/tflite-engine/src/test/java/ai/djl/tflite/engine/TfLiteTest.java
+++ b/engines/tflite/tflite-engine/src/test/java/ai/djl/tflite/engine/TfLiteTest.java
@@ -20,10 +20,10 @@ import ai.djl.modality.cv.ImageFactory;
 import ai.djl.repository.zoo.Criteria;
 import ai.djl.repository.zoo.ModelNotFoundException;
 import ai.djl.repository.zoo.ZooModel;
+import ai.djl.testing.TestRequirements;
 import ai.djl.translate.TranslateException;
 import java.io.IOException;
 import org.testng.Assert;
-import org.testng.SkipException;
 import org.testng.annotations.Test;
 
 public class TfLiteTest {
@@ -32,9 +32,8 @@ public class TfLiteTest {
     void testTflite()
             throws IOException, MalformedModelException, TranslateException,
                     ModelNotFoundException {
-        if (System.getProperty("os.name").toLowerCase().startsWith("win")) {
-            throw new SkipException("test only work on mac and Linux");
-        }
+        TestRequirements.notWindows();
+
         Criteria<Image, Classifications> criteria =
                 Criteria.builder()
                         .setTypes(Image.class, Classifications.class)
diff --git a/examples/src/test/java/ai/djl/examples/inference/BigGANTest.java b/examples/src/test/java/ai/djl/examples/inference/BigGANTest.java
index d023954b0..86d05a1cb 100644
--- a/examples/src/test/java/ai/djl/examples/inference/BigGANTest.java
+++ b/examples/src/test/java/ai/djl/examples/inference/BigGANTest.java
@@ -14,21 +14,19 @@ package ai.djl.examples.inference;
 
 import ai.djl.ModelException;
 import ai.djl.modality.cv.Image;
+import ai.djl.testing.TestRequirements;
 import ai.djl.translate.TranslateException;
 import java.io.IOException;
 import org.testng.Assert;
-import org.testng.SkipException;
 import org.testng.annotations.Test;
 
 public class BigGANTest {
 
     @Test
     public void testBigGAN() throws ModelException, TranslateException, IOException {
-        Image[] generatedImages = BigGAN.generate();
+        TestRequirements.engine("PyTorch");
 
-        if (generatedImages == null) {
-            throw new SkipException("Only works for PyTorch engine.");
-        }
+        Image[] generatedImages = BigGAN.generate();
 
         Assert.assertEquals(generatedImages.length, 5);
         for (Image img : generatedImages) {
diff --git a/examples/src/test/java/ai/djl/examples/inference/FeatureComparisonTest.java b/examples/src/test/java/ai/djl/examples/inference/FeatureComparisonTest.java
index 37433bd77..841b4d739 100644
--- a/examples/src/test/java/ai/djl/examples/inference/FeatureComparisonTest.java
+++ b/examples/src/test/java/ai/djl/examples/inference/FeatureComparisonTest.java
@@ -13,26 +13,23 @@
 package ai.djl.examples.inference;
 
 import ai.djl.ModelException;
-import ai.djl.engine.Engine;
 import ai.djl.examples.inference.face.FeatureComparison;
 import ai.djl.examples.inference.face.FeatureExtraction;
 import ai.djl.modality.cv.Image;
 import ai.djl.modality.cv.ImageFactory;
+import ai.djl.testing.TestRequirements;
 import ai.djl.translate.TranslateException;
 import java.io.IOException;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import org.testng.Assert;
-import org.testng.SkipException;
 import org.testng.annotations.Test;
 
 public class FeatureComparisonTest {
 
     @Test
     public void testFeatureComparison() throws ModelException, TranslateException, IOException {
-        if (!"PyTorch".equals(Engine.getInstance().getEngineName())) {
-            throw new SkipException("Only works for PyTorch engine.");
-        }
+        TestRequirements.engine("PyTorch");
 
         if (Boolean.getBoolean("nightly")) {
             Path imageFile1 = Paths.get("src/test/resources/kana1.jpg");
diff --git a/examples/src/test/java/ai/djl/examples/inference/FeatureExtractionTest.java b/examples/src/test/java/ai/djl/examples/inference/FeatureExtractionTest.java
index 33095dde9..ae93bb769 100644
--- a/examples/src/test/java/ai/djl/examples/inference/FeatureExtractionTest.java
+++ b/examples/src/test/java/ai/djl/examples/inference/FeatureExtractionTest.java
@@ -13,25 +13,22 @@
 package ai.djl.examples.inference;
 
 import ai.djl.ModelException;
-import ai.djl.engine.Engine;
 import ai.djl.examples.inference.face.FeatureExtraction;
 import ai.djl.modality.cv.Image;
 import ai.djl.modality.cv.ImageFactory;
+import ai.djl.testing.TestRequirements;
 import ai.djl.translate.TranslateException;
 import java.io.IOException;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import org.testng.Assert;
-import org.testng.SkipException;
 import org.testng.annotations.Test;
 
 public class FeatureExtractionTest {
 
     @Test
     public void testFeatureComparison() throws ModelException, TranslateException, IOException {
-        if (!"PyTorch".equals(Engine.getInstance().getEngineName())) {
-            throw new SkipException("Only works for PyTorch engine.");
-        }
+        TestRequirements.engine("PyTorch");
 
         Path imageFile = Paths.get("src/test/resources/kana1.jpg");
         Image img = ImageFactory.getInstance().fromFile(imageFile);
diff --git a/examples/src/test/java/ai/djl/examples/inference/LightFaceDetectionTest.java b/examples/src/test/java/ai/djl/examples/inference/LightFaceDetectionTest.java
index 6fd03763e..bd98572f8 100644
--- a/examples/src/test/java/ai/djl/examples/inference/LightFaceDetectionTest.java
+++ b/examples/src/test/java/ai/djl/examples/inference/LightFaceDetectionTest.java
@@ -13,25 +13,22 @@
 package ai.djl.examples.inference;
 
 import ai.djl.ModelException;
-import ai.djl.engine.Engine;
 import ai.djl.examples.inference.face.LightFaceDetection;
 import ai.djl.modality.Classifications;
 import ai.djl.modality.cv.output.DetectedObjects;
+import ai.djl.testing.TestRequirements;
 import ai.djl.translate.TranslateException;
 import java.io.IOException;
 import java.util.Collections;
 import java.util.List;
 import org.testng.Assert;
-import org.testng.SkipException;
 import org.testng.annotations.Test;
 
 public class LightFaceDetectionTest {
 
     @Test
     public void testLightFaceDetection() throws ModelException, TranslateException, IOException {
-        if (!"PyTorch".equals(Engine.getInstance().getEngineName())) {
-            throw new SkipException("Only works for PyTorch engine.");
-        }
+        TestRequirements.engine("PyTorch");
 
         if (Boolean.getBoolean("nightly")) {
             DetectedObjects result = LightFaceDetection.predict();
diff --git a/examples/src/test/java/ai/djl/examples/inference/ObjectDetectionWithTensorflowSavedModelTest.java b/examples/src/test/java/ai/djl/examples/inference/ObjectDetectionWithTensorflowSavedModelTest.java
index 3b240f3aa..0a1565175 100644
--- a/examples/src/test/java/ai/djl/examples/inference/ObjectDetectionWithTensorflowSavedModelTest.java
+++ b/examples/src/test/java/ai/djl/examples/inference/ObjectDetectionWithTensorflowSavedModelTest.java
@@ -16,12 +16,12 @@ package ai.djl.examples.inference;
 import ai.djl.ModelException;
 import ai.djl.modality.Classifications;
 import ai.djl.modality.cv.output.DetectedObjects;
+import ai.djl.testing.TestRequirements;
 import ai.djl.translate.TranslateException;
 import java.io.IOException;
 import java.util.Arrays;
 import java.util.List;
 import org.testng.Assert;
-import org.testng.SkipException;
 import org.testng.annotations.Test;
 
 public class ObjectDetectionWithTensorflowSavedModelTest {
@@ -30,19 +30,16 @@ public class ObjectDetectionWithTensorflowSavedModelTest {
     public void testObjectDetection() throws ModelException, TranslateException, IOException {
         // Only run nightly, this example download the synset file from github, this can cause
         // throttling and will fail the test.
-        if (Boolean.getBoolean("nightly")) {
-            DetectedObjects result = ObjectDetectionWithTensorflowSavedModel.predict();
-            // only works for TensorFlow
-            if (result == null) {
-                throw new SkipException("Only works for TensorFlow engine.");
-            }
+        TestRequirements.nightly();
+        TestRequirements.engine("TensorFlow");
 
-            Assert.assertEquals(result.getNumberOfObjects(), 3);
-            List<String> objects = Arrays.asList("dog", "bicycle", "car");
-            for (Classifications.Classification obj : result.items()) {
-                Assert.assertTrue(objects.contains(obj.getClassName()));
-                Assert.assertTrue(Double.compare(obj.getProbability(), 0.7) > 0);
-            }
+        DetectedObjects result = ObjectDetectionWithTensorflowSavedModel.predict();
+
+        Assert.assertEquals(result.getNumberOfObjects(), 3);
+        List<String> objects = Arrays.asList("dog", "bicycle", "car");
+        for (Classifications.Classification obj : result.items()) {
+            Assert.assertTrue(objects.contains(obj.getClassName()));
+            Assert.assertTrue(Double.compare(obj.getProbability(), 0.7) > 0);
         }
     }
 }
diff --git a/examples/src/test/java/ai/djl/examples/inference/RetinaFaceDetectionTest.java b/examples/src/test/java/ai/djl/examples/inference/RetinaFaceDetectionTest.java
index 7d02e89dd..ded180607 100644
--- a/examples/src/test/java/ai/djl/examples/inference/RetinaFaceDetectionTest.java
+++ b/examples/src/test/java/ai/djl/examples/inference/RetinaFaceDetectionTest.java
@@ -13,34 +13,29 @@
 package ai.djl.examples.inference;
 
 import ai.djl.ModelException;
-import ai.djl.engine.Engine;
 import ai.djl.examples.inference.face.RetinaFaceDetection;
 import ai.djl.modality.Classifications;
 import ai.djl.modality.cv.output.DetectedObjects;
+import ai.djl.testing.TestRequirements;
 import ai.djl.translate.TranslateException;
 import java.io.IOException;
 import java.util.Collections;
 import java.util.List;
 import org.testng.Assert;
-import org.testng.SkipException;
 import org.testng.annotations.Test;
 
 public class RetinaFaceDetectionTest {
 
     @Test
     public void testRetinaFaceDetection() throws ModelException, TranslateException, IOException {
-        if (!"PyTorch".equals(Engine.getInstance().getEngineName())) {
-            throw new SkipException("Only works for PyTorch engine.");
-        }
-
-        if (Boolean.getBoolean("nightly")) {
-            DetectedObjects result = RetinaFaceDetection.predict();
+        TestRequirements.engine("PyTorch");
+        TestRequirements.nightly();
+        DetectedObjects result = RetinaFaceDetection.predict();
 
-            List<String> objects = Collections.singletonList("Face");
-            for (Classifications.Classification obj : result.items()) {
-                Assert.assertTrue(objects.contains(obj.getClassName()));
-                Assert.assertTrue(Double.compare(obj.getProbability(), 0.6) > 0);
-            }
+        List<String> objects = Collections.singletonList("Face");
+        for (Classifications.Classification obj : result.items()) {
+            Assert.assertTrue(objects.contains(obj.getClassName()));
+            Assert.assertTrue(Double.compare(obj.getProbability(), 0.6) > 0);
         }
     }
 }
diff --git a/examples/src/test/java/ai/djl/examples/inference/SentimentAnalysisTest.java b/examples/src/test/java/ai/djl/examples/inference/SentimentAnalysisTest.java
index 30bd966a2..c7bda824f 100644
--- a/examples/src/test/java/ai/djl/examples/inference/SentimentAnalysisTest.java
+++ b/examples/src/test/java/ai/djl/examples/inference/SentimentAnalysisTest.java
@@ -13,24 +13,19 @@
 package ai.djl.examples.inference;
 
 import ai.djl.ModelException;
-import ai.djl.engine.Engine;
 import ai.djl.modality.Classifications;
+import ai.djl.testing.TestRequirements;
 import ai.djl.translate.TranslateException;
 import java.io.IOException;
 import org.testng.Assert;
-import org.testng.SkipException;
 import org.testng.annotations.Test;
 
 public class SentimentAnalysisTest {
 
     @Test
     public void testSentimentAnalysis() throws ModelException, TranslateException, IOException {
-        if (!Boolean.getBoolean("nightly")) {
-            throw new SkipException("Nightly only");
-        }
-        if (!"PyTorch".equals(Engine.getInstance().getEngineName())) {
-            throw new SkipException("Only works for PyTorch engine.");
-        }
+        TestRequirements.nightly();
+        TestRequirements.engine("PyTorch");
 
         Classifications result = SentimentAnalysis.predict();
         Assert.assertEquals(result.best().getClassName(), "Positive");
diff --git a/examples/src/test/java/ai/djl/examples/inference/StyleTransferTest.java b/examples/src/test/java/ai/djl/examples/inference/StyleTransferTest.java
index 96ba6e82e..ccfa07342 100644
--- a/examples/src/test/java/ai/djl/examples/inference/StyleTransferTest.java
+++ b/examples/src/test/java/ai/djl/examples/inference/StyleTransferTest.java
@@ -16,11 +16,11 @@ import ai.djl.MalformedModelException;
 import ai.djl.modality.cv.Image;
 import ai.djl.modality.cv.ImageFactory;
 import ai.djl.repository.zoo.ModelNotFoundException;
+import ai.djl.testing.TestRequirements;
 import ai.djl.translate.TranslateException;
 import java.io.IOException;
 import java.nio.file.Paths;
 import org.testng.Assert;
-import org.testng.SkipException;
 import org.testng.annotations.Test;
 
 public class StyleTransferTest {
@@ -29,14 +29,12 @@ public class StyleTransferTest {
     public void testStyleTransfer()
             throws IOException, ModelNotFoundException, MalformedModelException,
                     TranslateException {
+        TestRequirements.engine("PyTorch");
+
         String imagePath = "src/test/resources/mountains.png";
         Image input = ImageFactory.getInstance().fromFile(Paths.get(imagePath));
         Image generatedImage = StyleTransfer.transfer(input, StyleTransfer.Artist.MONET);
 
-        if (generatedImage == null) {
-            throw new SkipException("Only works for PyTorch engine.");
-        }
-
         Assert.assertEquals(generatedImage.getWidth(), input.getWidth());
         Assert.assertEquals(generatedImage.getHeight(), input.getHeight());
     }
diff --git a/examples/src/test/java/ai/djl/examples/inference/SuperResolutionTest.java b/examples/src/test/java/ai/djl/examples/inference/SuperResolutionTest.java
index 5d3b390a2..92147685d 100644
--- a/examples/src/test/java/ai/djl/examples/inference/SuperResolutionTest.java
+++ b/examples/src/test/java/ai/djl/examples/inference/SuperResolutionTest.java
@@ -16,29 +16,27 @@ import ai.djl.ModelException;
 import ai.djl.examples.inference.sr.SuperResolution;
 import ai.djl.modality.cv.Image;
 import ai.djl.modality.cv.ImageFactory;
+import ai.djl.testing.TestRequirements;
 import ai.djl.translate.TranslateException;
 import java.io.IOException;
 import java.nio.file.Paths;
 import java.util.Arrays;
 import java.util.List;
 import org.testng.Assert;
-import org.testng.SkipException;
 import org.testng.annotations.Test;
 
 public class SuperResolutionTest {
 
     @Test
     public void testSuperResolution() throws ModelException, TranslateException, IOException {
+        TestRequirements.engine("TensorFlow");
+
         String imagePath = "src/test/resources/";
         Image fox = ImageFactory.getInstance().fromFile(Paths.get(imagePath + "fox.png"));
         List<Image> inputImages = Arrays.asList(fox, fox);
 
         List<Image> enhancedImages = SuperResolution.enhance(inputImages);
 
-        if (enhancedImages == null) {
-            throw new SkipException("Only works for TensorFlow engine.");
-        }
-
         Assert.assertEquals(enhancedImages.size(), 2);
         int size = 4 * fox.getWidth();
         for (Image img : enhancedImages) {
diff --git a/examples/src/test/java/ai/djl/examples/inference/UniversalSentenceEncoderTest.java b/examples/src/test/java/ai/djl/examples/inference/UniversalSentenceEncoderTest.java
index 703f27628..957b2c930 100644
--- a/examples/src/test/java/ai/djl/examples/inference/UniversalSentenceEncoderTest.java
+++ b/examples/src/test/java/ai/djl/examples/inference/UniversalSentenceEncoderTest.java
@@ -13,25 +13,20 @@
 package ai.djl.examples.inference;
 
 import ai.djl.ModelException;
-import ai.djl.engine.Engine;
+import ai.djl.testing.TestRequirements;
 import ai.djl.translate.TranslateException;
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.List;
 import org.testng.Assert;
-import org.testng.SkipException;
 import org.testng.annotations.Test;
 
 public class UniversalSentenceEncoderTest {
 
     @Test
     public void testSentimentAnalysis() throws ModelException, TranslateException, IOException {
-        if (!Boolean.getBoolean("nightly")) {
-            throw new SkipException("Nightly only");
-        }
-        if (!"TensorFlow".equals(Engine.getInstance().getEngineName())) {
-            throw new SkipException("Only works for TensorFlow engine.");
-        }
+        TestRequirements.nightly();
+        TestRequirements.engine("TensorFlow");
 
         List<String> inputs = new ArrayList<>();
         inputs.add("The quick brown fox jumps over the lazy dog.");
diff --git a/examples/src/test/java/ai/djl/examples/training/TrainAmazonReviewTest.java b/examples/src/test/java/ai/djl/examples/training/TrainAmazonReviewTest.java
index 88ac834be..80b2c8e62 100644
--- a/examples/src/test/java/ai/djl/examples/training/TrainAmazonReviewTest.java
+++ b/examples/src/test/java/ai/djl/examples/training/TrainAmazonReviewTest.java
@@ -15,10 +15,10 @@ package ai.djl.examples.training;
 import ai.djl.ModelException;
 import ai.djl.engine.Engine;
 import ai.djl.examples.training.transferlearning.TrainAmazonReviewRanking;
+import ai.djl.testing.TestRequirements;
 import ai.djl.translate.TranslateException;
 import java.io.IOException;
 import java.net.URISyntaxException;
-import org.testng.SkipException;
 import org.testng.annotations.Test;
 
 public class TrainAmazonReviewTest {
@@ -26,10 +26,8 @@ public class TrainAmazonReviewTest {
     @Test
     public void testRankTraining()
             throws ModelException, TranslateException, IOException, URISyntaxException {
-        // this is nightly test
-        if (!Boolean.getBoolean("nightly")) {
-            throw new SkipException("Nightly only");
-        }
+        TestRequirements.nightly();
+
         String[] args;
         if (Engine.getInstance().getGpuCount() > 0) {
             args = new String[] {"-e", "1", "-g", "1"};
diff --git a/examples/src/test/java/ai/djl/examples/training/TrainPikachuTest.java b/examples/src/test/java/ai/djl/examples/training/TrainPikachuTest.java
index 585f92f51..b24ceec9c 100644
--- a/examples/src/test/java/ai/djl/examples/training/TrainPikachuTest.java
+++ b/examples/src/test/java/ai/djl/examples/training/TrainPikachuTest.java
@@ -14,21 +14,19 @@ package ai.djl.examples.training;
 
 import ai.djl.MalformedModelException;
 import ai.djl.engine.Engine;
+import ai.djl.testing.TestRequirements;
 import ai.djl.training.TrainingResult;
 import ai.djl.translate.TranslateException;
 import java.io.IOException;
 import org.testng.Assert;
-import org.testng.SkipException;
 import org.testng.annotations.Test;
 
 public class TrainPikachuTest {
 
     @Test
     public void testDetection() throws IOException, MalformedModelException, TranslateException {
-        // this is nightly test
-        if (!Boolean.getBoolean("nightly")) {
-            throw new SkipException("Nightly only");
-        }
+        TestRequirements.nightly();
+
         String[] args;
         float expectedLoss = 0;
         int expectedMinNumber = 0;
diff --git a/examples/src/test/java/ai/djl/examples/training/TrainResNetTest.java b/examples/src/test/java/ai/djl/examples/training/TrainResNetTest.java
index eb71ea4fa..1848266a2 100644
--- a/examples/src/test/java/ai/djl/examples/training/TrainResNetTest.java
+++ b/examples/src/test/java/ai/djl/examples/training/TrainResNetTest.java
@@ -16,11 +16,11 @@ package ai.djl.examples.training;
 import ai.djl.ModelException;
 import ai.djl.engine.Engine;
 import ai.djl.examples.training.transferlearning.TrainResnetWithCifar10;
+import ai.djl.testing.TestRequirements;
 import ai.djl.training.TrainingResult;
 import ai.djl.translate.TranslateException;
 import java.io.IOException;
 import org.testng.Assert;
-import org.testng.SkipException;
 import org.testng.annotations.Test;
 
 public class TrainResNetTest {
@@ -39,44 +39,38 @@ public class TrainResNetTest {
     @Test
     public void testTrainResNetSymbolicNightly()
             throws ModelException, IOException, TranslateException {
-        // this is nightly test
-        if (!Boolean.getBoolean("nightly")) {
-            throw new SkipException("Nightly only");
-        }
-        if (Engine.getInstance().getGpuCount() > 0) {
-            // Limit max 4 gpu for cifar10 training to make it converge faster.
-            // and only train 10 batch for unit test.
-            String[] args = {"-e", "10", "-g", "4", "-s", "-p"};
+        TestRequirements.nightly();
+        TestRequirements.gpu();
 
-            Engine.getInstance().setRandomSeed(SEED);
-            TrainingResult result = TrainResnetWithCifar10.runExample(args);
-            Assert.assertNotNull(result);
+        // Limit max 4 gpu for cifar10 training to make it converge faster.
+        // and only train 10 batch for unit test.
+        String[] args = {"-e", "10", "-g", "4", "-s", "-p"};
+
+        Engine.getInstance().setRandomSeed(SEED);
+        TrainingResult result = TrainResnetWithCifar10.runExample(args);
+        Assert.assertNotNull(result);
 
-            Assert.assertTrue(result.getTrainEvaluation("Accuracy") >= 0.8f);
-            Assert.assertTrue(result.getValidateEvaluation("Accuracy") >= 0.68f);
-            Assert.assertTrue(result.getValidateLoss() < 1.1);
-        }
+        Assert.assertTrue(result.getTrainEvaluation("Accuracy") >= 0.8f);
+        Assert.assertTrue(result.getValidateEvaluation("Accuracy") >= 0.68f);
+        Assert.assertTrue(result.getValidateLoss() < 1.1);
     }
 
     @Test
     public void testTrainResNetImperativeNightly()
             throws ModelException, IOException, TranslateException {
-        // this is nightly test
-        if (!Boolean.getBoolean("nightly")) {
-            throw new SkipException("Nightly only");
-        }
-        if (Engine.getInstance().getGpuCount() > 0) {
-            // Limit max 4 gpu for cifar10 training to make it converge faster.
-            // and only train 10 batch for unit test.
-            String[] args = {"-e", "10", "-g", "4"};
+        TestRequirements.nightly();
+        TestRequirements.gpu();
 
-            Engine.getInstance().setRandomSeed(SEED);
-            TrainingResult result = TrainResnetWithCifar10.runExample(args);
-            Assert.assertNotNull(result);
+        // Limit max 4 gpu for cifar10 training to make it converge faster.
+        // and only train 10 batch for unit test.
+        String[] args = {"-e", "10", "-g", "4"};
+
+        Engine.getInstance().setRandomSeed(SEED);
+        TrainingResult result = TrainResnetWithCifar10.runExample(args);
+        Assert.assertNotNull(result);
 
-            Assert.assertTrue(result.getTrainEvaluation("Accuracy") >= 0.9f);
-            Assert.assertTrue(result.getValidateEvaluation("Accuracy") >= 0.75f);
-            Assert.assertTrue(result.getValidateLoss() < 1);
-        }
+        Assert.assertTrue(result.getTrainEvaluation("Accuracy") >= 0.9f);
+        Assert.assertTrue(result.getValidateEvaluation("Accuracy") >= 0.75f);
+        Assert.assertTrue(result.getValidateLoss() < 1);
     }
 }
diff --git a/examples/src/test/java/ai/djl/examples/training/TrainSentimentAnalysisTest.java b/examples/src/test/java/ai/djl/examples/training/TrainSentimentAnalysisTest.java
index 4f6858ef6..3ee9357d5 100644
--- a/examples/src/test/java/ai/djl/examples/training/TrainSentimentAnalysisTest.java
+++ b/examples/src/test/java/ai/djl/examples/training/TrainSentimentAnalysisTest.java
@@ -13,11 +13,10 @@
 package ai.djl.examples.training;
 
 import ai.djl.MalformedModelException;
-import ai.djl.engine.Engine;
 import ai.djl.repository.zoo.ModelNotFoundException;
+import ai.djl.testing.TestRequirements;
 import ai.djl.translate.TranslateException;
 import java.io.IOException;
-import org.testng.SkipException;
 import org.testng.annotations.Test;
 
 public class TrainSentimentAnalysisTest {
@@ -26,13 +25,8 @@ public class TrainSentimentAnalysisTest {
     public void testTrainSentimentAnalysis()
             throws MalformedModelException, ModelNotFoundException, TranslateException,
                     IOException {
-        // this is nightly test
-        if (!Boolean.getBoolean("nightly")) {
-            throw new SkipException("Nightly only");
-        }
-        if (Engine.getInstance().getGpuCount() == 0) {
-            throw new SkipException("GPU only");
-        }
+        TestRequirements.nightly();
+        TestRequirements.gpu();
 
         String[] args = new String[] {"-e", "1", "-g", "1"};
         TrainSentimentAnalysis.runExample(args);
diff --git a/extensions/fasttext/src/test/java/ai/djl/fasttext/CookingStackExchangeTest.java b/extensions/fasttext/src/test/java/ai/djl/fasttext/CookingStackExchangeTest.java
index faade9423..0ab6d1180 100644
--- a/extensions/fasttext/src/test/java/ai/djl/fasttext/CookingStackExchangeTest.java
+++ b/extensions/fasttext/src/test/java/ai/djl/fasttext/CookingStackExchangeTest.java
@@ -23,6 +23,7 @@ import ai.djl.ndarray.types.Shape;
 import ai.djl.repository.zoo.Criteria;
 import ai.djl.repository.zoo.ModelNotFoundException;
 import ai.djl.repository.zoo.ZooModel;
+import ai.djl.testing.TestRequirements;
 import ai.djl.training.TrainingResult;
 import java.io.IOException;
 import java.io.InputStream;
@@ -35,7 +36,6 @@ import java.util.Collections;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.testng.Assert;
-import org.testng.SkipException;
 import org.testng.annotations.Test;
 
 public class CookingStackExchangeTest {
@@ -44,9 +44,8 @@ public class CookingStackExchangeTest {
 
     @Test
     public void testTrainTextClassification() throws IOException {
-        if (System.getProperty("os.name").toLowerCase().contains("win")) {
-            throw new SkipException("fastText is not supported on windows");
-        }
+        TestRequirements.notWindows(); // fastText is not supported on windows
+
         try (FtModel model = new FtModel("cooking")) {
             CookingStackExchange dataset = CookingStackExchange.builder().build();
 
@@ -68,9 +67,8 @@ public class CookingStackExchangeTest {
     @Test
     public void testTextClassification()
             throws IOException, MalformedModelException, ModelNotFoundException {
-        if (System.getProperty("os.name").toLowerCase().contains("win")) {
-            throw new SkipException("fastText is not supported on windows");
-        }
+        TestRequirements.notWindows(); // fastText is not supported on windows
+
         Criteria<String, Classifications> criteria =
                 Criteria.builder()
                         .setTypes(String.class, Classifications.class)
@@ -87,9 +85,8 @@ public class CookingStackExchangeTest {
 
     @Test
     public void testWord2Vec() throws IOException, MalformedModelException, ModelNotFoundException {
-        if (System.getProperty("os.name").toLowerCase().contains("win")) {
-            throw new SkipException("fastText is not supported on windows");
-        }
+        TestRequirements.notWindows(); // fastText is not supported on windows
+
         Criteria<String, Classifications> criteria =
                 Criteria.builder()
                         .setTypes(String.class, Classifications.class)
@@ -111,12 +108,8 @@ public class CookingStackExchangeTest {
 
     @Test
     public void testBlazingText() throws IOException, ModelException {
-        if (System.getProperty("os.name").toLowerCase().contains("win")) {
-            throw new SkipException("fastText is not supported on windows");
-        }
-        if (!Boolean.getBoolean("nightly")) {
-            throw new SkipException("Nightly only");
-        }
+        TestRequirements.notWindows(); // fastText is not supported on windows
+        TestRequirements.nightly();
 
         URL url = new URL("https://resources.djl.ai/test-models/blazingtext_classification.bin");
         Path path = Paths.get("build/tmp/model");
diff --git a/extensions/hadoop/src/test/java/ai/djl/hadoop/hdfs/HdfsRepositoryTest.java b/extensions/hadoop/src/test/java/ai/djl/hadoop/hdfs/HdfsRepositoryTest.java
index 14046e67c..19938e13e 100644
--- a/extensions/hadoop/src/test/java/ai/djl/hadoop/hdfs/HdfsRepositoryTest.java
+++ b/extensions/hadoop/src/test/java/ai/djl/hadoop/hdfs/HdfsRepositoryTest.java
@@ -16,6 +16,7 @@ import ai.djl.Application;
 import ai.djl.repository.Artifact;
 import ai.djl.repository.MRL;
 import ai.djl.repository.Repository;
+import ai.djl.testing.TestRequirements;
 import ai.djl.util.Utils;
 import ai.djl.util.ZipUtils;
 import java.io.IOException;
@@ -28,7 +29,6 @@ import org.apache.hadoop.fs.FileSystem;
 import org.apache.hadoop.fs.Path;
 import org.apache.hadoop.hdfs.MiniDFSCluster;
 import org.testng.Assert;
-import org.testng.SkipException;
 import org.testng.annotations.AfterClass;
 import org.testng.annotations.BeforeClass;
 import org.testng.annotations.Test;
@@ -39,9 +39,7 @@ public class HdfsRepositoryTest {
 
     @BeforeClass
     public void setup() throws IOException {
-        if (System.getProperty("os.name").startsWith("Win")) {
-            throw new SkipException("MiniDFSCluster doesn't wupport windows.");
-        }
+        TestRequirements.notWindows(); // MiniDFSCluster doesn't wupport windows.
 
         System.setProperty("DJL_CACHE_DIR", "build/cache");
         String userHome = System.getProperty("user.home");
diff --git a/extensions/sentencepiece/src/test/java/ai/djl/sentencepiece/SpTextEmbeddingTest.java b/extensions/sentencepiece/src/test/java/ai/djl/sentencepiece/SpTextEmbeddingTest.java
index 8b1088413..811e1e216 100644
--- a/extensions/sentencepiece/src/test/java/ai/djl/sentencepiece/SpTextEmbeddingTest.java
+++ b/extensions/sentencepiece/src/test/java/ai/djl/sentencepiece/SpTextEmbeddingTest.java
@@ -12,6 +12,7 @@
  */
 package ai.djl.sentencepiece;
 
+import ai.djl.testing.TestRequirements;
 import ai.djl.training.util.DownloadUtils;
 import java.io.IOException;
 import java.nio.file.Files;
@@ -19,7 +20,6 @@ import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.Collections;
 import org.testng.Assert;
-import org.testng.SkipException;
 import org.testng.annotations.BeforeTest;
 import org.testng.annotations.Test;
 
@@ -36,9 +36,8 @@ public class SpTextEmbeddingTest {
 
     @Test
     public void testEmbedding() throws IOException {
-        if (System.getProperty("os.name").startsWith("Win")) {
-            throw new SkipException("Skip windows test.");
-        }
+        TestRequirements.notWindows();
+
         Path modelPath = Paths.get("build/test/models/sententpiece_test_model.model");
         try (SpTokenizer tokenizer = new SpTokenizer(modelPath)) {
             SpTextEmbedding embedding = SpTextEmbedding.from(tokenizer);
diff --git a/extensions/sentencepiece/src/test/java/ai/djl/sentencepiece/SpTokenizerTest.java b/extensions/sentencepiece/src/test/java/ai/djl/sentencepiece/SpTokenizerTest.java
index 873a6b784..1b2f97f22 100644
--- a/extensions/sentencepiece/src/test/java/ai/djl/sentencepiece/SpTokenizerTest.java
+++ b/extensions/sentencepiece/src/test/java/ai/djl/sentencepiece/SpTokenizerTest.java
@@ -13,6 +13,7 @@
 
 package ai.djl.sentencepiece;
 
+import ai.djl.testing.TestRequirements;
 import ai.djl.training.util.DownloadUtils;
 import java.io.IOException;
 import java.nio.file.Files;
@@ -21,7 +22,6 @@ import java.nio.file.Paths;
 import java.util.Arrays;
 import java.util.List;
 import org.testng.Assert;
-import org.testng.SkipException;
 import org.testng.annotations.BeforeTest;
 import org.testng.annotations.Test;
 
@@ -39,9 +39,8 @@ public class SpTokenizerTest {
 
     @Test
     public void testTokenize() throws IOException {
-        if (System.getProperty("os.name").startsWith("Win")) {
-            throw new SkipException("Skip windows test.");
-        }
+        TestRequirements.notWindows();
+
         Path modelPath = Paths.get("build/test/models/sententpiece_test_model.model");
         try (SpTokenizer tokenizer = new SpTokenizer(modelPath)) {
             String original = "Hello World";
@@ -56,9 +55,8 @@ public class SpTokenizerTest {
     @Test
     @SuppressWarnings("AvoidEscapedUnicodeCharacters")
     public void testUtf16Tokenize() throws IOException {
-        if (System.getProperty("os.name").startsWith("Win")) {
-            throw new SkipException("Skip windows test.");
-        }
+        TestRequirements.notWindows();
+
         Path modelPath = Paths.get("build/test/models/sententpiece_test_model.model");
         try (SpTokenizer tokenizer = new SpTokenizer(modelPath)) {
             String original = "\uD83D\uDC4B\uD83D\uDC4B";
@@ -70,9 +68,8 @@ public class SpTokenizerTest {
 
     @Test
     public void testEncodeDecode() throws IOException {
-        if (System.getProperty("os.name").startsWith("Win")) {
-            throw new SkipException("Skip windows test.");
-        }
+        TestRequirements.notWindows();
+
         Path modelPath = Paths.get("build/test/models");
         String prefix = "sententpiece_test_model";
         try (SpTokenizer tokenizer = new SpTokenizer(modelPath, prefix)) {
diff --git a/extensions/sentencepiece/src/test/java/ai/djl/sentencepiece/SpVocabularyTest.java b/extensions/sentencepiece/src/test/java/ai/djl/sentencepiece/SpVocabularyTest.java
index 773e6de81..ec3eaddd6 100644
--- a/extensions/sentencepiece/src/test/java/ai/djl/sentencepiece/SpVocabularyTest.java
+++ b/extensions/sentencepiece/src/test/java/ai/djl/sentencepiece/SpVocabularyTest.java
@@ -13,13 +13,13 @@
 
 package ai.djl.sentencepiece;
 
+import ai.djl.testing.TestRequirements;
 import ai.djl.training.util.DownloadUtils;
 import java.io.IOException;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import org.testng.Assert;
-import org.testng.SkipException;
 import org.testng.annotations.BeforeTest;
 import org.testng.annotations.Test;
 
@@ -37,9 +37,8 @@ public class SpVocabularyTest {
 
     @Test
     public void testTokenIdConversion() throws IOException {
-        if (System.getProperty("os.name").startsWith("Win")) {
-            throw new SkipException("Skip windows test.");
-        }
+        TestRequirements.notWindows();
+
         Path modelPath = Paths.get("build/test/models");
         String prefix = "sententpiece_test_model";
         SpTokenizer tokenizer = new SpTokenizer(modelPath, prefix);
diff --git a/integration/src/main/java/ai/djl/integration/tests/modality/cv/NDImageUtilsTest.java b/integration/src/main/java/ai/djl/integration/tests/modality/cv/NDImageUtilsTest.java
index 6f142e36d..4326c9fcf 100644
--- a/integration/src/main/java/ai/djl/integration/tests/modality/cv/NDImageUtilsTest.java
+++ b/integration/src/main/java/ai/djl/integration/tests/modality/cv/NDImageUtilsTest.java
@@ -12,7 +12,6 @@
  */
 package ai.djl.integration.tests.modality.cv;
 
-import ai.djl.engine.Engine;
 import ai.djl.modality.cv.Image;
 import ai.djl.modality.cv.util.NDImageUtils;
 import ai.djl.ndarray.NDArray;
@@ -20,18 +19,17 @@ import ai.djl.ndarray.NDManager;
 import ai.djl.ndarray.index.NDIndex;
 import ai.djl.ndarray.types.Shape;
 import ai.djl.testing.Assertions;
+import ai.djl.testing.TestRequirements;
 import ai.djl.util.cuda.CudaUtils;
 import org.testng.Assert;
-import org.testng.SkipException;
 import org.testng.annotations.Test;
 
 public class NDImageUtilsTest {
 
     @Test
     public void testNormalize() {
-        if ("TensorFlow".equals(Engine.getInstance().getEngineName())) {
-            throw new SkipException("TensorFlow use channels last by default.");
-        }
+        TestRequirements.notEngine("TensorFlow"); // TensorFlow use channels last by default
+
         try (NDManager manager = NDManager.newBaseManager()) {
             // test 3D C, H, W
             NDArray image = manager.ones(new Shape(3, 4, 2));
@@ -67,9 +65,8 @@ public class NDImageUtilsTest {
 
     @Test
     public void testToTensor() {
-        if ("TensorFlow".equals(Engine.getInstance().getEngineName())) {
-            throw new SkipException("TensorFlow use channels last by default.");
-        }
+        TestRequirements.notEngine("TensorFlow"); // TensorFlow use channels last by default
+
         try (NDManager manager = NDManager.newBaseManager()) {
             // test 3D C, H, W
             NDArray image = manager.randomUniform(0, 255, new Shape(4, 2, 3));
diff --git a/integration/src/main/java/ai/djl/integration/tests/model_zoo/ImperativeModelZooTest.java b/integration/src/main/java/ai/djl/integration/tests/model_zoo/ImperativeModelZooTest.java
index 222cc215f..e58f179c8 100644
--- a/integration/src/main/java/ai/djl/integration/tests/model_zoo/ImperativeModelZooTest.java
+++ b/integration/src/main/java/ai/djl/integration/tests/model_zoo/ImperativeModelZooTest.java
@@ -17,7 +17,6 @@ import ai.djl.MalformedModelException;
 import ai.djl.Model;
 import ai.djl.basicmodelzoo.BasicModelZoo;
 import ai.djl.basicmodelzoo.cv.classification.AlexNet;
-import ai.djl.integration.util.TestUtils;
 import ai.djl.modality.Classifications;
 import ai.djl.modality.cv.Image;
 import ai.djl.modality.cv.output.DetectedObjects;
@@ -25,12 +24,12 @@ import ai.djl.ndarray.types.Shape;
 import ai.djl.nn.Block;
 import ai.djl.repository.zoo.Criteria;
 import ai.djl.repository.zoo.ModelNotFoundException;
+import ai.djl.testing.TestRequirements;
 import ai.djl.training.DefaultTrainingConfig;
 import ai.djl.training.Trainer;
 import ai.djl.training.loss.SoftmaxCrossEntropyLoss;
 import java.io.IOException;
 import org.testng.Assert;
-import org.testng.SkipException;
 import org.testng.annotations.Test;
 
 public class ImperativeModelZooTest {
@@ -38,10 +37,8 @@ public class ImperativeModelZooTest {
     @Test
     public void testImperativeModelInputOutput()
             throws MalformedModelException, ModelNotFoundException, IOException {
-        // Test imperative models, only available in MXNet engine
-        if (!TestUtils.isMxnet()) {
-            throw new SkipException("Resnet50-cifar10 model only available in MXNet");
-        }
+        TestRequirements.engine("MXNet"); // Resnet50-cifar10 model only available in MXNet
+
         // from model zoo
         Criteria<Image, Classifications> criteria =
                 Criteria.builder()
diff --git a/integration/src/main/java/ai/djl/integration/tests/model_zoo/ModelZooTest.java b/integration/src/main/java/ai/djl/integration/tests/model_zoo/ModelZooTest.java
index e5887510e..b52239d67 100644
--- a/integration/src/main/java/ai/djl/integration/tests/model_zoo/ModelZooTest.java
+++ b/integration/src/main/java/ai/djl/integration/tests/model_zoo/ModelZooTest.java
@@ -19,12 +19,11 @@ import ai.djl.repository.Artifact;
 import ai.djl.repository.zoo.Criteria;
 import ai.djl.repository.zoo.ModelLoader;
 import ai.djl.repository.zoo.ModelZoo;
+import ai.djl.testing.TestRequirements;
 import ai.djl.util.Utils;
 import java.io.IOException;
 import java.nio.file.Paths;
-import java.util.Calendar;
 import java.util.List;
-import org.testng.SkipException;
 import org.testng.annotations.AfterClass;
 import org.testng.annotations.BeforeClass;
 import org.testng.annotations.Test;
@@ -47,12 +46,9 @@ public class ModelZooTest {
 
     @Test
     public void testDownloadModels() throws IOException, ModelException {
-        if (!Boolean.getBoolean("nightly") || Boolean.getBoolean("offline")) {
-            throw new SkipException("Weekly only");
-        }
-        if (Calendar.SATURDAY != Calendar.getInstance().get(Calendar.DAY_OF_WEEK)) {
-            throw new SkipException("Weekly only");
-        }
+        TestRequirements.nightly();
+        TestRequirements.notOffline();
+        TestRequirements.weekly();
 
         for (ModelZoo zoo : ModelZoo.listModelZoo()) {
             for (ModelLoader modelLoader : zoo.getModelLoaders()) {
diff --git a/integration/src/main/java/ai/djl/integration/tests/model_zoo/classification/ResnetTest.java b/integration/src/main/java/ai/djl/integration/tests/model_zoo/classification/ResnetTest.java
index 1d02905c4..49afb20cb 100644
--- a/integration/src/main/java/ai/djl/integration/tests/model_zoo/classification/ResnetTest.java
+++ b/integration/src/main/java/ai/djl/integration/tests/model_zoo/classification/ResnetTest.java
@@ -19,7 +19,6 @@ import ai.djl.basicmodelzoo.BasicModelZoo;
 import ai.djl.basicmodelzoo.cv.classification.ResNetV1;
 import ai.djl.engine.Engine;
 import ai.djl.inference.Predictor;
-import ai.djl.integration.util.TestUtils;
 import ai.djl.modality.Classifications;
 import ai.djl.modality.cv.Image;
 import ai.djl.ndarray.NDArray;
@@ -32,6 +31,7 @@ import ai.djl.repository.zoo.Criteria;
 import ai.djl.repository.zoo.ModelNotFoundException;
 import ai.djl.repository.zoo.ZooModel;
 import ai.djl.testing.Assertions;
+import ai.djl.testing.TestRequirements;
 import ai.djl.training.DefaultTrainingConfig;
 import ai.djl.training.EasyTrain;
 import ai.djl.training.Trainer;
@@ -46,7 +46,6 @@ import ai.djl.util.PairList;
 import java.io.IOException;
 import java.util.Collections;
 import java.util.List;
-import org.testng.SkipException;
 import org.testng.annotations.Test;
 
 public class ResnetTest {
@@ -151,9 +150,7 @@ public class ResnetTest {
 
     private ZooModel<Image, Classifications> getModel()
             throws IOException, ModelNotFoundException, MalformedModelException {
-        if (!TestUtils.isMxnet()) {
-            throw new SkipException("Resnet50-cifar10 model only available in MXNet");
-        }
+        TestRequirements.engine("MXNet"); // Resnet50-cifar10 model only available in MXNet
 
         Criteria<Image, Classifications> criteria =
                 Criteria.builder()
diff --git a/integration/src/main/java/ai/djl/integration/tests/model_zoo/classification/VGGTest.java b/integration/src/main/java/ai/djl/integration/tests/model_zoo/classification/VGGTest.java
index ceccffbca..a92bd7b0b 100644
--- a/integration/src/main/java/ai/djl/integration/tests/model_zoo/classification/VGGTest.java
+++ b/integration/src/main/java/ai/djl/integration/tests/model_zoo/classification/VGGTest.java
@@ -22,6 +22,7 @@ import ai.djl.ndarray.types.DataType;
 import ai.djl.ndarray.types.Shape;
 import ai.djl.nn.Block;
 import ai.djl.nn.Parameter;
+import ai.djl.testing.TestRequirements;
 import ai.djl.training.DefaultTrainingConfig;
 import ai.djl.training.EasyTrain;
 import ai.djl.training.ParameterStore;
@@ -35,7 +36,6 @@ import ai.djl.util.PairList;
 import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
 import org.testng.Assert;
-import org.testng.SkipException;
 import org.testng.annotations.Test;
 
 public class VGGTest {
@@ -44,9 +44,8 @@ public class VGGTest {
     public void testTrainWithDefaultChannels() {
         // Vgg is too large to be fit into the github action cpu main memory
         // only run the test when we have GPU
-        if (Engine.getInstance().getGpuCount() == 0) {
-            throw new SkipException("GPU only");
-        }
+        TestRequirements.gpu();
+
         TrainingConfig config =
                 new DefaultTrainingConfig(Loss.softmaxCrossEntropyLoss())
                         .optDevices(Engine.getInstance().getDevices(2));
diff --git a/integration/src/main/java/ai/djl/integration/tests/model_zoo/object_detection/SingleShotDetectionTest.java b/integration/src/main/java/ai/djl/integration/tests/model_zoo/object_detection/SingleShotDetectionTest.java
index 1d0821321..12acab0cd 100644
--- a/integration/src/main/java/ai/djl/integration/tests/model_zoo/object_detection/SingleShotDetectionTest.java
+++ b/integration/src/main/java/ai/djl/integration/tests/model_zoo/object_detection/SingleShotDetectionTest.java
@@ -19,7 +19,6 @@ import ai.djl.basicdataset.cv.PikachuDetection;
 import ai.djl.basicmodelzoo.BasicModelZoo;
 import ai.djl.engine.Engine;
 import ai.djl.inference.Predictor;
-import ai.djl.integration.util.TestUtils;
 import ai.djl.modality.cv.Image;
 import ai.djl.modality.cv.ImageFactory;
 import ai.djl.modality.cv.MultiBoxDetection;
@@ -34,6 +33,7 @@ import ai.djl.nn.SequentialBlock;
 import ai.djl.repository.zoo.Criteria;
 import ai.djl.repository.zoo.ModelNotFoundException;
 import ai.djl.repository.zoo.ZooModel;
+import ai.djl.testing.TestRequirements;
 import ai.djl.training.DefaultTrainingConfig;
 import ai.djl.training.EasyTrain;
 import ai.djl.training.Trainer;
@@ -49,7 +49,6 @@ import java.io.IOException;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import org.testng.Assert;
-import org.testng.SkipException;
 import org.testng.annotations.Test;
 
 public class SingleShotDetectionTest {
@@ -129,9 +128,7 @@ public class SingleShotDetectionTest {
 
     private ZooModel<Image, DetectedObjects> getModel()
             throws IOException, ModelNotFoundException, MalformedModelException {
-        if (!TestUtils.isMxnet()) {
-            throw new SkipException("SSD-pikachu model only available in MXNet");
-        }
+        TestRequirements.engine("MXNet"); // SSD-pikachu model only available in MXNet
 
         Criteria<Image, DetectedObjects> criteria =
                 Criteria.builder()
diff --git a/integration/src/main/java/ai/djl/integration/tests/ndarray/NDArrayCreationOpTest.java b/integration/src/main/java/ai/djl/integration/tests/ndarray/NDArrayCreationOpTest.java
index 71cf7c816..49087c850 100644
--- a/integration/src/main/java/ai/djl/integration/tests/ndarray/NDArrayCreationOpTest.java
+++ b/integration/src/main/java/ai/djl/integration/tests/ndarray/NDArrayCreationOpTest.java
@@ -21,12 +21,12 @@ import ai.djl.ndarray.types.DataType;
 import ai.djl.ndarray.types.Shape;
 import ai.djl.ndarray.types.SparseFormat;
 import ai.djl.testing.Assertions;
+import ai.djl.testing.TestRequirements;
 import ai.djl.util.Pair;
 import ai.djl.util.PairList;
 import java.nio.FloatBuffer;
 import java.util.stream.IntStream;
 import org.testng.Assert;
-import org.testng.SkipException;
 import org.testng.annotations.Test;
 
 public class NDArrayCreationOpTest {
@@ -425,10 +425,11 @@ public class NDArrayCreationOpTest {
 
     @Test
     public void testFixedSeed() {
+        TestRequirements.notEngine(
+                "TensorFlow"); // TensorFlow fixed random seed requires restart process.
+
         try (NDManager manager = NDManager.newBaseManager()) {
-            if ("TensorFlow".equals(Engine.getInstance().getEngineName())) {
-                throw new SkipException("TensorFlow fixed random seed require restart process.");
-            }
+
             int fixedSeed = 1234;
             Engine.getInstance().setRandomSeed(fixedSeed);
             NDArray expectedUniform = manager.randomUniform(-10, 10, new Shape(10, 10));
diff --git a/integration/src/main/java/ai/djl/integration/tests/nn/BlockCoreTest.java b/integration/src/main/java/ai/djl/integration/tests/nn/BlockCoreTest.java
index 9206dcdd4..27549a0c4 100644
--- a/integration/src/main/java/ai/djl/integration/tests/nn/BlockCoreTest.java
+++ b/integration/src/main/java/ai/djl/integration/tests/nn/BlockCoreTest.java
@@ -41,6 +41,7 @@ import ai.djl.nn.recurrent.GRU;
 import ai.djl.nn.recurrent.LSTM;
 import ai.djl.nn.recurrent.RNN;
 import ai.djl.testing.Assertions;
+import ai.djl.testing.TestRequirements;
 import ai.djl.training.DefaultTrainingConfig;
 import ai.djl.training.GradientCollector;
 import ai.djl.training.Trainer;
@@ -56,7 +57,6 @@ import java.io.IOException;
 import java.nio.file.Files;
 import java.util.Arrays;
 import org.testng.Assert;
-import org.testng.SkipException;
 import org.testng.annotations.Test;
 
 public class BlockCoreTest {
@@ -207,9 +207,7 @@ public class BlockCoreTest {
     @SuppressWarnings("try")
     @Test
     public void testLayerNorm() throws IOException, MalformedModelException {
-        if (!"PyTorch".equals(Engine.getInstance().getEngineName())) {
-            throw new SkipException("Only works for PyTorch engine.");
-        }
+        TestRequirements.engine("PyTorch");
 
         TrainingConfig config =
                 new DefaultTrainingConfig(Loss.l2Loss())
@@ -238,9 +236,7 @@ public class BlockCoreTest {
     @SuppressWarnings("try")
     @Test
     public void test2LayerNorm() throws IOException, MalformedModelException {
-        if (!"PyTorch".equals(Engine.getInstance().getEngineName())) {
-            throw new SkipException("Only works for PyTorch engine.");
-        }
+        TestRequirements.engine("PyTorch");
 
         TrainingConfig config =
                 new DefaultTrainingConfig(Loss.l2Loss())
diff --git a/integration/src/main/java/ai/djl/integration/tests/training/GradientCollectorIntegrationTest.java b/integration/src/main/java/ai/djl/integration/tests/training/GradientCollectorIntegrationTest.java
index 0f7e5df2e..bd55242fd 100644
--- a/integration/src/main/java/ai/djl/integration/tests/training/GradientCollectorIntegrationTest.java
+++ b/integration/src/main/java/ai/djl/integration/tests/training/GradientCollectorIntegrationTest.java
@@ -22,6 +22,7 @@ import ai.djl.nn.Blocks;
 import ai.djl.nn.Parameter;
 import ai.djl.nn.core.Linear;
 import ai.djl.testing.Assertions;
+import ai.djl.testing.TestRequirements;
 import ai.djl.training.DefaultTrainingConfig;
 import ai.djl.training.EasyTrain;
 import ai.djl.training.GradientCollector;
@@ -37,7 +38,6 @@ import ai.djl.training.tracker.Tracker;
 import ai.djl.translate.TranslateException;
 import java.io.IOException;
 import org.testng.Assert;
-import org.testng.SkipException;
 import org.testng.annotations.Test;
 
 public class GradientCollectorIntegrationTest {
@@ -75,9 +75,7 @@ public class GradientCollectorIntegrationTest {
 
     @Test
     public void testTrain() throws IOException, TranslateException {
-        if (!Boolean.getBoolean("nightly")) {
-            throw new SkipException("Nightly only");
-        }
+        TestRequirements.nightly();
 
         int numOfData = 1000;
         int batchSize = 10;
diff --git a/integration/src/main/java/ai/djl/integration/util/TestUtils.java b/integration/src/main/java/ai/djl/integration/util/TestUtils.java
index b4b7969df..217ff291a 100644
--- a/integration/src/main/java/ai/djl/integration/util/TestUtils.java
+++ b/integration/src/main/java/ai/djl/integration/util/TestUtils.java
@@ -29,11 +29,6 @@ public final class TestUtils {
         return System.getProperty("os.name").startsWith("Win");
     }
 
-    public static boolean isMxnet() {
-        Engine engine = Engine.getInstance();
-        return "MXNet".equals(engine.getEngineName());
-    }
-
     public static boolean isEngine(String name) {
         Engine engine = Engine.getInstance();
         return name.equals(engine.getEngineName());
@@ -53,7 +48,7 @@ public final class TestUtils {
 
     public static Device[] getDevices() {
         if (!Engine.getInstance().hasCapability(StandardCapabilities.CUDNN)
-                && TestUtils.isMxnet()) {
+                && "MXNet".equals(Engine.getInstance().getEngineName())) {
             return new Device[] {
                 Device.cpu()
             }; // TODO: RNN is not implemented on MXNet without cuDNN
diff --git a/testing/src/main/java/ai/djl/testing/TestRequirements.java b/testing/src/main/java/ai/djl/testing/TestRequirements.java
new file mode 100644
index 000000000..91d0472b1
--- /dev/null
+++ b/testing/src/main/java/ai/djl/testing/TestRequirements.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance
+ * with the License. A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0/
+ *
+ * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES
+ * OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions
+ * and limitations under the License.
+ */
+package ai.djl.testing;
+
+import ai.djl.engine.Engine;
+import java.util.Arrays;
+import java.util.Calendar;
+import org.testng.SkipException;
+
+/**
+ * This utility class is used to define test requirements.
+ *
+ * <p>When the test requirements are not fulfilled, the test is skipped with a {@link
+ * SkipException}.
+ */
+public final class TestRequirements {
+    private TestRequirements() {}
+
+    /** Requires a test runs as part of the nightly suite, but not standard local or CI builds. */
+    public static void nightly() {
+        if (!Boolean.getBoolean("nightly")) {
+            throw new SkipException("This test requires the nightly flag to run");
+        }
+    }
+
+    /** Requires a test only run one day per week (Saturday). */
+    public static void weekly() {
+        if (Calendar.SATURDAY != Calendar.getInstance().get(Calendar.DAY_OF_WEEK)) {
+            throw new SkipException("This test only runs one day per week (Saturday)");
+        }
+    }
+
+    /** Requires a test not be run in offline mode. */
+    public static void notOffline() {
+        if (Boolean.getBoolean("offline")) {
+            throw new SkipException("This test can not run while offline");
+        }
+    }
+
+    /**
+     * Requires a test only with the allowed engine(s).
+     *
+     * @param engines the engine(s) to run the test with
+     */
+    public static void engine(String... engines) {
+        String engineName = Engine.getInstance().getEngineName();
+        for (String e : engines) {
+            if (engineName.equals(e)) {
+                return;
+            }
+        }
+        throw new SkipException(
+                "This test requires one of the engines: " + Arrays.toString(engines));
+    }
+
+    /**
+     * Requires a test have any engines except for those listed.
+     *
+     * @param engines the engine(s) to not run the test on
+     */
+    public static void notEngine(String... engines) {
+        String engineName = Engine.getInstance().getEngineName();
+        for (String e : engines) {
+            if (engineName.equals(e)) {
+                throw new SkipException(
+                        "This test requires not using the engines: " + Arrays.toString(engines));
+            }
+        }
+    }
+
+    /** Requires a test have at least one gpu. */
+    public static void gpu() {
+        if (Engine.getInstance().getGpuCount() == 0) {
+            throw new SkipException("This test requires a GPU to run");
+        }
+    }
+
+    /** Requires that the test runs on OSX or linux, not windows. */
+    public static void notWindows() {
+        if (System.getProperty("os.name").toLowerCase().startsWith("win")) {
+            throw new SkipException("This test requires a non-windows os.");
+        }
+    }
+}