Repository: deepjavalibrary/djl
Pull request #1330 (2021-10-29 20:11:49)

Create TestRequirements utility
The TestRequirements simplifies the usage of SkipExceptions to skip various
tests. It helps reduce some code duplication of the requirements to concentrate
most of it within the same file. Finally, by aggregating the various
requirements into methods it makes it possible to use IDE findUsages to find all
of the tests with a particular requirement.

codecov-commenter: # [Codecov](https://codecov.io/gh/deepjavalibrary/djl/pull/1330?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=deepjavalibrary) Report
> Merging [#1330](https://codecov.io/gh/deepjavalibrary/djl/pull/1330?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=deepjavalibrary) (bc714b3) 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.48%`.
> The diff coverage is `55.44%`.

[![Impacted file tree graph](https://codecov.io/gh/deepjavalibrary/djl/pull/1330/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/1330?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=deepjavalibrary)

```diff
@@             Coverage Diff              @@
##             master    #1330      +/-   ##
============================================
- Coverage     72.08%   71.60%   -0.49%     
- Complexity     5126     5143      +17     
============================================
  Files           473      478       +5     
  Lines         21970    22256     +286     
  Branches       2351     2385      +34     
============================================
+ Hits          15838    15936      +98     
- Misses         4925     5111     +186     
- Partials       1207     1209       +2     
```


| [Impacted Files](https://codecov.io/gh/deepjavalibrary/djl/pull/1330?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/1330/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/1330/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/1330/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/1330/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/1330/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/1330/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/1330/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/1330/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/1330/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/1330/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 [85 more](https://codecov.io/gh/deepjavalibrary/djl/pull/1330/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/1330?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/1330?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=deepjavalibrary). Last update [866161a...bc714b3](https://codecov.io/gh/deepjavalibrary/djl/pull/1330?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/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/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/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.");
+        }
+    }
+}