diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 271b1daf7274419b0577a9dc37a862c8b3eab5ab..7e7f213e8fc2792a60cb1cbf6e45b0bac49190ed 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -3,6 +3,9 @@ image: gitlab-registry.irstea.fr/remi.cresson/otbtf:2.4-cpu-basic-testing
 variables:
     OTB_BUILD: /src/otb/build/OTB/build  # Local OTB build directory
     OTBTF_SRC: /src/otbtf  # Local OTBTF source directory
+    OTB_TEST_DIR: $OTB_BUILD/Testing/Temporary  # OTB testing directory
+    ARTIFACT_TEST_DIR: $CI_PROJECT_DIR/testing
+    CRC_BOOK_TMP: /tmp/crc_book_tests_tmp
 
 workflow:
   rules:
@@ -13,13 +16,17 @@ stages:
   - Build
   - Static Analysis
   - Test
+  - Applications Test
 
 .update_otbtf_src: &update_otbtf_src
   - sudo rm -rf $OTBTF_SRC && sudo ln -s $PWD $OTBTF_SRC  # Replace local OTBTF source directory
 
 .compile_otbtf: &compile_otbtf
   - cd $OTB_BUILD && sudo make install -j$(nproc --all)  # Rebuild OTB with new OTBTF sources
-
+  
+.install_pytest: &install_pytest
+  - pip3 install pytest pytest-cov pytest-order  # Install pytest stuff
+  
 before_script:
   - *update_otbtf_src
 
@@ -60,21 +67,47 @@ ctest:
   stage: Test
   script:
     - *compile_otbtf
-    - sudo rm -rf $OTB_BUILD/Testing/Temporary/*  # Empty testing temporary folder (old files here)
+    - sudo rm -rf $OTB_TEST_DIR/*  # Empty testing temporary folder (old files here)
     - cd $OTB_BUILD/ && sudo ctest -L OTBTensorflow  # Run ctest
   after_script:
-    - cp -r $OTB_BUILD/Testing/Temporary $CI_PROJECT_DIR/testing  # Copy artifacts (they must be in $CI_PROJECT_DIR)
+    - cp -r $OTB_TEST_DIR $ARTIFACT_TEST_DIR
   artifacts:
     paths:
-      - testing/*.*
+      - $ARTIFACT_TEST_DIR/*.*
     expire_in: 1 week
     when: on_failure
 
+.applications_test_base:
+  stage: Applications Test
+  rules:
+      # Only for MR targeting 'develop' branch because applications tests are slow
+    - if: $CI_MERGE_REQUEST_ID && $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == 'develop'
+  artifacts:
+    when: on_failure
+    paths:
+      - $CI_PROJECT_DIR/report_*.xml
+      - $ARTIFACT_TEST_DIR/*.*
+    expire_in: 1 week
+          
+crc_book:
+  extends: .applications_test_base
+  script:
+    - *compile_otbtf
+    - *install_pytest
+    - cd $CI_PROJECT_DIR
+    - mkdir -p $CRC_BOOK_TMP
+    - TMPDIR=$CRC_BOOK_TMP \
+      DATADIR=$CI_PROJECT_DIR/test/data \
+      python -m pytest --junitxml=$CI_PROJECT_DIR/report_tutorial.xml $OTBTF_SRC/test/tutorial_unittest.py
+  after_script:
+    - mkdir -p $ARTIFACT_TEST_DIR
+    - cp $CRC_BOOK_TMP/*.* $ARTIFACT_TEST_DIR/
+    
 sr4rs:
-  stage: Test
+  extends: .applications_test_base
   script:
     - *compile_otbtf
-    - pip3 install pytest pytest-cov
+    - *install_pytest
     - cd $CI_PROJECT_DIR
     - wget -O sr4rs_sentinel2_bands4328_france2020_savedmodel.zip
       https://nextcloud.inrae.fr/s/boabW9yCjdpLPGX/download/sr4rs_sentinel2_bands4328_france2020_savedmodel.zip
@@ -85,8 +118,4 @@ sr4rs:
     - git clone https://github.com/remicres/sr4rs.git
     - export PYTHONPATH=$PYTHONPATH:$PWD/sr4rs
     - python -m pytest --junitxml=$CI_PROJECT_DIR/report_sr4rs.xml $OTBTF_SRC/test/sr4rs_unittest.py
-  artifacts:
-    when: on_failure
-    paths:
-      - $CI_PROJECT_DIR/report_sr4rs.xml
-    expire_in: 1 week
+
diff --git a/test/data/RF_model_from_deep_features_map.tif b/test/data/RF_model_from_deep_features_map.tif
new file mode 100644
index 0000000000000000000000000000000000000000..d41d832c4d1e4f9848636d76f45cac26a1fdce15
Binary files /dev/null and b/test/data/RF_model_from_deep_features_map.tif differ
diff --git a/test/data/amsterdam_labels_A.tif b/test/data/amsterdam_labels_A.tif
new file mode 100644
index 0000000000000000000000000000000000000000..d07a0e0cfbcfad1f6b1b887ddf2a9470687c58be
Binary files /dev/null and b/test/data/amsterdam_labels_A.tif differ
diff --git a/test/data/amsterdam_labels_B.tif b/test/data/amsterdam_labels_B.tif
new file mode 100644
index 0000000000000000000000000000000000000000..b7af54a4c573aa9bbf1c7407e7dac9597a5a19e2
Binary files /dev/null and b/test/data/amsterdam_labels_B.tif differ
diff --git a/test/data/amsterdam_patches_A.tif b/test/data/amsterdam_patches_A.tif
new file mode 100644
index 0000000000000000000000000000000000000000..e8d2e70ece62fd8c1e20820b74c0edf2f16f80dd
Binary files /dev/null and b/test/data/amsterdam_patches_A.tif differ
diff --git a/test/data/amsterdam_patches_B.tif b/test/data/amsterdam_patches_B.tif
new file mode 100644
index 0000000000000000000000000000000000000000..d6b20fc81a85a57f09af319aa74af7e5e7cf76dc
Binary files /dev/null and b/test/data/amsterdam_patches_B.tif differ
diff --git a/test/data/classif_model1.tif b/test/data/classif_model1.tif
new file mode 100644
index 0000000000000000000000000000000000000000..a681cfd6a22867778b5b5452ab881f0352d567ba
Binary files /dev/null and b/test/data/classif_model1.tif differ
diff --git a/test/data/classif_model2.tif b/test/data/classif_model2.tif
new file mode 100644
index 0000000000000000000000000000000000000000..9b23ede1e26a72a52c843038f56b7b762e4f0d1b
Binary files /dev/null and b/test/data/classif_model2.tif differ
diff --git a/test/data/classif_model3_fcn.tif b/test/data/classif_model3_fcn.tif
new file mode 100644
index 0000000000000000000000000000000000000000..8109d0d7432b321055c9562d613710603dbd4b99
Binary files /dev/null and b/test/data/classif_model3_fcn.tif differ
diff --git a/test/data/classif_model3_pb.tif b/test/data/classif_model3_pb.tif
new file mode 100644
index 0000000000000000000000000000000000000000..028c5cefaf60344b80aeafe7c6bc03b19034f206
Binary files /dev/null and b/test/data/classif_model3_pb.tif differ
diff --git a/test/data/classif_model4.tif b/test/data/classif_model4.tif
new file mode 100644
index 0000000000000000000000000000000000000000..b87fe1e3f61613301b2d496fdd0ff41e5c66da53
Binary files /dev/null and b/test/data/classif_model4.tif differ
diff --git a/test/data/fake_spot6.jp2 b/test/data/fake_spot6.jp2
new file mode 100644
index 0000000000000000000000000000000000000000..6464ca60b7a86fc18b0be664885466ab064c6262
Binary files /dev/null and b/test/data/fake_spot6.jp2 differ
diff --git a/test/data/outvec_A.gpkg b/test/data/outvec_A.gpkg
new file mode 100644
index 0000000000000000000000000000000000000000..87c8301d562985ceb5bc97f0c899cfd4c07de294
Binary files /dev/null and b/test/data/outvec_A.gpkg differ
diff --git a/test/data/outvec_B.gpkg b/test/data/outvec_B.gpkg
new file mode 100644
index 0000000000000000000000000000000000000000..78578a3a204cb393478905492dd8b859aaa5691e
Binary files /dev/null and b/test/data/outvec_B.gpkg differ
diff --git a/test/data/s2_10m_labels_A.tif b/test/data/s2_10m_labels_A.tif
new file mode 100644
index 0000000000000000000000000000000000000000..1af82324acce9247ecb86283c05d52f0a616a3de
Binary files /dev/null and b/test/data/s2_10m_labels_A.tif differ
diff --git a/test/data/s2_10m_labels_B.tif b/test/data/s2_10m_labels_B.tif
new file mode 100644
index 0000000000000000000000000000000000000000..de91615723f14adb16ac024fce92b1be32f4103b
Binary files /dev/null and b/test/data/s2_10m_labels_B.tif differ
diff --git a/test/data/s2_10m_patches_A.tif b/test/data/s2_10m_patches_A.tif
new file mode 100644
index 0000000000000000000000000000000000000000..c0567ff9282fb6628a1d9200fd9d63143b1c2487
Binary files /dev/null and b/test/data/s2_10m_patches_A.tif differ
diff --git a/test/data/s2_10m_patches_B.tif b/test/data/s2_10m_patches_B.tif
new file mode 100644
index 0000000000000000000000000000000000000000..056fe19ee93c6a892bb78f9174e014d0377ba3a9
Binary files /dev/null and b/test/data/s2_10m_patches_B.tif differ
diff --git a/test/data/s2_20m_patches_A.tif b/test/data/s2_20m_patches_A.tif
new file mode 100644
index 0000000000000000000000000000000000000000..2f299c2f5b1b4eed702d0c7d9bb542169b760434
Binary files /dev/null and b/test/data/s2_20m_patches_A.tif differ
diff --git a/test/data/s2_20m_patches_B.tif b/test/data/s2_20m_patches_B.tif
new file mode 100644
index 0000000000000000000000000000000000000000..afd4ac9775c191983af6cbea1da82943bace7be2
Binary files /dev/null and b/test/data/s2_20m_patches_B.tif differ
diff --git a/test/data/s2_20m_stack.jp2 b/test/data/s2_20m_stack.jp2
new file mode 100644
index 0000000000000000000000000000000000000000..c518a4ba71845802ebd6f443d18f626da22e5175
Binary files /dev/null and b/test/data/s2_20m_stack.jp2 differ
diff --git a/test/data/s2_labels_A.tif b/test/data/s2_labels_A.tif
new file mode 100644
index 0000000000000000000000000000000000000000..c4451320fad48a811d7b9a1b221502013d4a822f
Binary files /dev/null and b/test/data/s2_labels_A.tif differ
diff --git a/test/data/s2_labels_B.tif b/test/data/s2_labels_B.tif
new file mode 100644
index 0000000000000000000000000000000000000000..2b90eb45f97cf3e65045fcca00b95ec8aa7c0a77
Binary files /dev/null and b/test/data/s2_labels_B.tif differ
diff --git a/test/data/s2_patches_A.tif b/test/data/s2_patches_A.tif
new file mode 100644
index 0000000000000000000000000000000000000000..4f338e312214e89bf85e4aafe4b20588918dcc6f
Binary files /dev/null and b/test/data/s2_patches_A.tif differ
diff --git a/test/data/s2_patches_B.tif b/test/data/s2_patches_B.tif
new file mode 100644
index 0000000000000000000000000000000000000000..022d525e31db3713f4f10264411a0cfbbe76cf74
Binary files /dev/null and b/test/data/s2_patches_B.tif differ
diff --git a/test/data/s2_stack.jp2 b/test/data/s2_stack.jp2
new file mode 100644
index 0000000000000000000000000000000000000000..cd5864ee2583441036aeec6bce96eec6b0533ebd
Binary files /dev/null and b/test/data/s2_stack.jp2 differ
diff --git a/test/data/terrain_truth_epsg32654_A.tif b/test/data/terrain_truth_epsg32654_A.tif
new file mode 100644
index 0000000000000000000000000000000000000000..87a9a74bd6a337cac61559e2682b53676fecbd66
Binary files /dev/null and b/test/data/terrain_truth_epsg32654_A.tif differ
diff --git a/test/data/terrain_truth_epsg32654_B.tif b/test/data/terrain_truth_epsg32654_B.tif
new file mode 100644
index 0000000000000000000000000000000000000000..2114099b6a815fd3fd5d74449488e8e4ed98ca19
Binary files /dev/null and b/test/data/terrain_truth_epsg32654_B.tif differ
diff --git a/test/sr4rs_unittest.py b/test/sr4rs_unittest.py
index fbb921f8451cc83b3fd7b9e9e90bf61755511eca..89c3945f153304d04d35c23ef34513cf668120bc 100644
--- a/test/sr4rs_unittest.py
+++ b/test/sr4rs_unittest.py
@@ -4,8 +4,7 @@
 import unittest
 import os
 from pathlib import Path
-import gdal
-import otbApplication as otb
+import test_utils
 
 
 def command_train_succeed(extra_opts=""):
@@ -54,21 +53,7 @@ class SR4RSv1Test(unittest.TestCase):
         command += "--output '{}?&box=256:256:512:512'".format(out_img)
         os.system(command)
 
-        nbchannels_reconstruct = gdal.Open(out_img).RasterCount
-        nbchannels_baseline = gdal.Open(baseline).RasterCount
-
-        self.assertTrue(nbchannels_reconstruct == nbchannels_baseline)
-
-        for i in range(1, 1 + nbchannels_baseline):
-            comp = otb.Registry.CreateApplication('CompareImages')
-            comp.SetParameterString('ref.in', baseline)
-            comp.SetParameterInt('ref.channel', i)
-            comp.SetParameterString('meas.in', out_img)
-            comp.SetParameterInt('meas.channel', i)
-            comp.Execute()
-            mae = comp.GetParameterFloat('mae')
-
-            self.assertTrue(mae < 0.01)
+        self.assertTrue(test_utils.compare(baseline, out_img))
 
 
 if __name__ == '__main__':
diff --git a/test/test_utils.py b/test/test_utils.py
new file mode 100644
index 0000000000000000000000000000000000000000..c07301e91bb2eab4a808d53beaa46396c3ea82fc
--- /dev/null
+++ b/test/test_utils.py
@@ -0,0 +1,56 @@
+import otbApplication
+import os
+
+
+def get_nb_of_channels(raster):
+    """
+    Return the number of channels in the input raster
+    :param raster: raster filename (str)
+    :return the number of channels in the image (int)
+    """
+    info = otbApplication.Registry.CreateApplication("ReadImageInfo")
+    info.SetParameterString("in", raster)
+    info.ExecuteAndWriteOutput()
+    return info.GetParameterInt('numberbands')
+
+
+def compare(raster1, raster2, tol=0.01):
+    """
+    Return True if the two rasters have the same contents in each bands
+    :param raster1: raster 1 filename (str)
+    :param raster2: raster 2 filename (str)
+    :param tol: tolerance (float)
+    """
+    n_bands1 = get_nb_of_channels(raster1)
+    n_bands2 = get_nb_of_channels(raster2)
+    if n_bands1 != n_bands2:
+        print("The images have not the same number of channels")
+        return False
+
+    for i in range(1, 1 + n_bands1):
+        comp = otbApplication.Registry.CreateApplication('CompareImages')
+        comp.SetParameterString('ref.in', raster1)
+        comp.SetParameterInt('ref.channel', i)
+        comp.SetParameterString('meas.in', raster2)
+        comp.SetParameterInt('meas.channel', i)
+        comp.Execute()
+        mae = comp.GetParameterFloat('mae')
+        if mae > tol:
+            print("The images have not the same content in channel {} "
+                  "(Mean average error: {})".format(i, mae))
+            return False
+    return True
+
+
+def resolve_paths(filename, var_list):
+    """
+    Retrieve environment variables in paths
+    :param filename: file name
+    :params var_list: variable list
+    :return filename with retrieved environment variables
+    """
+    new_filename = filename
+    for var in var_list:
+        new_filename = new_filename.replace("${}".format(var), os.environ[var])
+    print("Resolve filename...\n\tfilename: {}, \n\tnew filename: {}".format(filename, new_filename))
+    return new_filename
diff --git a/test/tutorial_unittest.py b/test/tutorial_unittest.py
new file mode 100644
index 0000000000000000000000000000000000000000..7934862f348326de13a11eef4c81a4fe6512cec6
--- /dev/null
+++ b/test/tutorial_unittest.py
@@ -0,0 +1,513 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+import pytest
+import unittest
+import os
+from pathlib import Path
+import test_utils
+
+INFERENCE_MAE_TOL = 10.0  # Dummy value: we don't really care of the mae value but rather the image size etc
+
+
+def resolve_paths(path):
+    """
+    Resolve a path with the environment variables
+    """
+    return test_utils.resolve_paths(path, var_list=["TMPDIR", "DATADIR"])
+
+
+def run_command(command):
+    """
+    Run a command
+    :param command: the command to run
+    """
+    full_command = resolve_paths(command)
+    print("Running command: \n\t {}".format(full_command))
+    os.system(full_command)
+
+
+def run_command_and_test_exist(command, file_list):
+    """
+    :param command: the command to run (str)
+    :param file_list: list of files to check
+    :return True or False
+    """
+    run_command(command)
+    print("Checking if files exist...")
+    for file in file_list:
+        print("\t{}".format(file))
+        path = Path(resolve_paths(file))
+        if not path.is_file():
+            print("File {} does not exist!".format(file))
+            return False
+        print("\tOk")
+    return True
+
+
+def run_command_and_compare(command, to_compare_dict, tol=0.01):
+    """
+    :param command: the command to run (str)
+    :param to_compare_dict: a dict of {baseline1: output1, ..., baselineN: outputN}
+    :param tol: tolerance (float)
+    :return True or False
+    """
+
+    run_command(command)
+    for baseline, output in to_compare_dict.items():
+        if not test_utils.compare(resolve_paths(baseline), resolve_paths(output), tol):
+            print("Baseline {} and output {} differ.".format(baseline, output))
+            return False
+    return True
+
+
+class TutorialTest(unittest.TestCase):
+
+    @pytest.mark.order(1)
+    def test_sample_selection(self):
+        self.assertTrue(
+            run_command_and_test_exist(
+                command="otbcli_LabelImageSampleSelection "
+                        "-inref $DATADIR/terrain_truth_epsg32654_A.tif "
+                        "-nodata 255 "
+                        "-outvec $TMPDIR/outvec_A.gpkg",
+                file_list=["$TMPDIR/outvec_A.gpkg"]))
+        self.assertTrue(
+            run_command_and_test_exist(
+                command="otbcli_LabelImageSampleSelection "
+                        "-inref $DATADIR/terrain_truth_epsg32654_B.tif "
+                        "-nodata 255 "
+                        "-outvec $TMPDIR/outvec_B.gpkg",
+                file_list=["$TMPDIR/outvec_B.gpkg"]))
+
+    @pytest.mark.order(2)
+    def test_patches_extraction(self):
+        self.assertTrue(
+            run_command_and_compare(
+                command="otbcli_PatchesExtraction "
+                        "-source1.il $DATADIR/s2_stack.jp2 "
+                        "-source1.out $TMPDIR/s2_patches_A.tif "
+                        "-source1.patchsizex 16 "
+                        "-source1.patchsizey 16 "
+                        "-vec $TMPDIR/outvec_A.gpkg "
+                        "-field class "
+                        "-outlabels $TMPDIR/s2_labels_A.tif",
+                to_compare_dict={"$DATADIR/s2_patches_A.tif": "$TMPDIR/s2_patches_A.tif",
+                                 "$DATADIR/s2_labels_A.tif": "$TMPDIR/s2_labels_A.tif"}))
+        self.assertTrue(
+            run_command_and_compare(
+                command="otbcli_PatchesExtraction "
+                        "-source1.il $DATADIR/s2_stack.jp2 "
+                        "-source1.out $TMPDIR/s2_patches_B.tif "
+                        "-source1.patchsizex 16 "
+                        "-source1.patchsizey 16 "
+                        "-vec $TMPDIR/outvec_B.gpkg "
+                        "-field class "
+                        "-outlabels $TMPDIR/s2_labels_B.tif",
+                to_compare_dict={"$DATADIR/s2_patches_B.tif": "$TMPDIR/s2_patches_B.tif",
+                                 "$DATADIR/s2_labels_B.tif": "$TMPDIR/s2_labels_B.tif"}))
+
+    @pytest.mark.order(3)
+    def test_generate_model1(self):
+        run_command("git clone https://github.com/remicres/otbtf_tutorials_resources.git "
+                    "$TMPDIR/otbtf_tuto_repo")
+        self.assertTrue(
+            run_command_and_test_exist(
+                command="python $TMPDIR/otbtf_tuto_repo/01_patch_based_classification/models/create_model1.py "
+                        "$TMPDIR/model1",
+                file_list=["$TMPDIR/model1/saved_model.pb"]))
+
+    @pytest.mark.order(4)
+    def test_model1_train(self):
+        self.assertTrue(
+            run_command_and_test_exist(
+                command="otbcli_TensorflowModelTrain "
+                        "-training.source1.il $DATADIR/s2_patches_A.tif "
+                        "-training.source1.patchsizex 16 "
+                        "-training.source1.patchsizey 16 "
+                        "-training.source1.placeholder x "
+                        "-training.source2.il $DATADIR/s2_labels_A.tif "
+                        "-training.source2.patchsizex 1 "
+                        "-training.source2.patchsizey 1 "
+                        "-training.source2.placeholder y "
+                        "-model.dir $TMPDIR/model1 "
+                        "-training.targetnodes optimizer "
+                        "-training.epochs 10 "
+                        "-validation.mode class "
+                        "-validation.source1.il $DATADIR/s2_patches_B.tif "
+                        "-validation.source1.name x "
+                        "-validation.source2.il $DATADIR/s2_labels_B.tif "
+                        "-validation.source2.name prediction "
+                        "-model.saveto $TMPDIR/model1/variables/variables",
+                file_list=["$TMPDIR/model1/variables/variables.index"]
+            )
+        )
+
+    @pytest.mark.order(5)
+    def test_model1_inference_pb(self):
+        self.assertTrue(
+            run_command_and_compare(
+                command="otbcli_TensorflowModelServe "
+                        "-source1.il $DATADIR/s2_stack.jp2 "
+                        "-source1.rfieldx 16 "
+                        "-source1.rfieldy 16 "
+                        "-source1.placeholder x "
+                        "-model.dir $TMPDIR/model1 "
+                        "-output.names prediction "
+                        "-out \"$TMPDIR/classif_model1.tif?&box=4000:4000:1000:1000\" uint8",
+                to_compare_dict={"$DATADIR/classif_model1.tif": "$TMPDIR/classif_model1.tif"},
+                tol=INFERENCE_MAE_TOL))
+
+    @pytest.mark.order(6)
+    def test_model1_inference_fcn(self):
+        self.assertTrue(
+            run_command_and_compare(
+                command="otbcli_TensorflowModelServe "
+                        "-source1.il $DATADIR/s2_stack.jp2 "
+                        "-source1.rfieldx 16 "
+                        "-source1.rfieldy 16 "
+                        "-source1.placeholder x "
+                        "-model.dir $TMPDIR/model1 "
+                        "-output.names prediction "
+                        "-model.fullyconv on "
+                        "-output.spcscale 4 "
+                        "-out \"$TMPDIR/classif_model1.tif?&box=1000:1000:256:256\" uint8",
+                to_compare_dict={"$DATADIR/classif_model1.tif": "$TMPDIR/classif_model1.tif"},
+                tol=INFERENCE_MAE_TOL))
+
+    @pytest.mark.order(7)
+    def test_rf_sampling(self):
+        self.assertTrue(
+            run_command_and_test_exist(
+                command="otbcli_SampleExtraction "
+                        "-in $DATADIR/s2_stack.jp2 "
+                        "-vec $TMPDIR/outvec_A.gpkg "
+                        "-field class "
+                        "-out $TMPDIR/pixelvalues_A.gpkg",
+                file_list=["$TMPDIR/pixelvalues_A.gpkg"]))
+        self.assertTrue(
+            run_command_and_test_exist(
+                command="otbcli_SampleExtraction "
+                        "-in $DATADIR/s2_stack.jp2 "
+                        "-vec $TMPDIR/outvec_B.gpkg "
+                        "-field class "
+                        "-out $TMPDIR/pixelvalues_B.gpkg",
+                file_list=["$TMPDIR/pixelvalues_B.gpkg"]))
+
+    @pytest.mark.order(8)
+    def test_rf_training(self):
+        self.assertTrue(
+            run_command_and_test_exist(
+                command="otbcli_TrainVectorClassifier "
+                        "-io.vd $TMPDIR/pixelvalues_A.gpkg "
+                        "-valid.vd $TMPDIR/pixelvalues_B.gpkg "
+                        "-feat value_0 value_1 value_2 value_3 "
+                        "-cfield class "
+                        "-classifier rf "
+                        "-io.out $TMPDIR/randomforest_model.yaml ",
+                file_list=["$TMPDIR/randomforest_model.yaml"]))
+
+    @pytest.mark.order(9)
+    def test_generate_model2(self):
+        self.assertTrue(
+            run_command_and_test_exist(
+                command="python $TMPDIR/otbtf_tuto_repo/01_patch_based_classification/models/create_model2.py "
+                        "$TMPDIR/model2",
+                file_list=["$TMPDIR/model2/saved_model.pb"]))
+
+    @pytest.mark.order(10)
+    def test_model2_train(self):
+        self.assertTrue(
+            run_command_and_test_exist(
+                command="otbcli_TensorflowModelTrain "
+                        "-training.source1.il $DATADIR/s2_patches_A.tif "
+                        "-training.source1.patchsizex 16 "
+                        "-training.source1.patchsizey 16 "
+                        "-training.source1.placeholder x "
+                        "-training.source2.il $DATADIR/s2_labels_A.tif "
+                        "-training.source2.patchsizex 1 "
+                        "-training.source2.patchsizey 1 "
+                        "-training.source2.placeholder y "
+                        "-model.dir $TMPDIR/model2 "
+                        "-training.targetnodes optimizer "
+                        "-training.epochs 10 "
+                        "-validation.mode class "
+                        "-validation.source1.il $DATADIR/s2_patches_B.tif "
+                        "-validation.source1.name x "
+                        "-validation.source2.il $DATADIR/s2_labels_B.tif "
+                        "-validation.source2.name prediction "
+                        "-model.saveto $TMPDIR/model2/variables/variables",
+                file_list=["$TMPDIR/model2/variables/variables.index"]))
+
+    @pytest.mark.order(11)
+    def test_model2_inference_fcn(self):
+        self.assertTrue(
+            run_command_and_compare(command="otbcli_TensorflowModelServe "
+                                            "-source1.il $DATADIR/s2_stack.jp2 "
+                                            "-source1.rfieldx 16 "
+                                            "-source1.rfieldy 16 "
+                                            "-source1.placeholder x "
+                                            "-model.dir $TMPDIR/model2 "
+                                            "-model.fullyconv on "
+                                            "-output.names prediction "
+                                            "-out \"$TMPDIR/classif_model2.tif?&box=4000:4000:1000:1000\" uint8",
+                                    to_compare_dict={"$DATADIR/classif_model2.tif": "$TMPDIR/classif_model2.tif"},
+                                    tol=INFERENCE_MAE_TOL))
+
+    @pytest.mark.order(12)
+    def test_model2rf_train(self):
+        self.assertTrue(
+            run_command_and_test_exist(
+                command="otbcli_TrainClassifierFromDeepFeatures "
+                        "-source1.il $DATADIR/s2_stack.jp2 "
+                        "-source1.rfieldx 16 "
+                        "-source1.rfieldy 16 "
+                        "-source1.placeholder x "
+                        "-model.dir $TMPDIR/model2 "
+                        "-model.fullyconv on "
+                        "-optim.tilesizex 999999 "
+                        "-optim.tilesizey 128 "
+                        "-output.names features "
+                        "-vd $TMPDIR/outvec_A.gpkg "
+                        "-valid $TMPDIR/outvec_B.gpkg "
+                        "-sample.vfn class "
+                        "-sample.bm 0 "
+                        "-classifier rf "
+                        "-out $TMPDIR/RF_model_from_deep_features.yaml",
+                file_list=["$TMPDIR/RF_model_from_deep_features.yaml"]))
+
+    @pytest.mark.order(13)
+    def test_model2rf_inference(self):
+        self.assertTrue(
+            run_command_and_compare(
+                command="otbcli_ImageClassifierFromDeepFeatures "
+                        "-source1.il $DATADIR/s2_stack.jp2 "
+                        "-source1.rfieldx 16 "
+                        "-source1.rfieldy 16 "
+                        "-source1.placeholder x "
+                        "-deepmodel.dir $TMPDIR/model2 "
+                        "-deepmodel.fullyconv on "
+                        "-output.names features "
+                        "-model $TMPDIR/RF_model_from_deep_features.yaml "
+                        "-out \"$TMPDIR/RF_model_from_deep_features_map.tif?&box=4000:4000:1000:1000\" uint8",
+                to_compare_dict={
+                    "$DATADIR/RF_model_from_deep_features_map.tif": "$TMPDIR/RF_model_from_deep_features_map.tif"},
+                tol=INFERENCE_MAE_TOL))
+
+    @pytest.mark.order(14)
+    def test_patch_extraction_20m(self):
+        self.assertTrue(
+            run_command_and_compare(
+                command="OTB_TF_NSOURCES=2 otbcli_PatchesExtraction "
+                        "-source1.il $DATADIR/s2_20m_stack.jp2 "
+                        "-source1.patchsizex 8 "
+                        "-source1.patchsizey 8 "
+                        "-source1.out $TMPDIR/s2_20m_patches_A.tif "
+                        "-source2.il $DATADIR/s2_stack.jp2 "
+                        "-source2.patchsizex 16 "
+                        "-source2.patchsizey 16 "
+                        "-source2.out $TMPDIR/s2_10m_patches_A.tif "
+                        "-vec $TMPDIR/outvec_A.gpkg "
+                        "-field class "
+                        "-outlabels $TMPDIR/s2_10m_labels_A.tif uint8",
+                to_compare_dict={"$DATADIR/s2_10m_labels_A.tif": "$TMPDIR/s2_10m_labels_A.tif",
+                                 "$DATADIR/s2_10m_patches_A.tif": "$TMPDIR/s2_10m_patches_A.tif",
+                                 "$DATADIR/s2_20m_patches_A.tif": "$TMPDIR/s2_20m_patches_A.tif"}))
+        self.assertTrue(
+            run_command_and_compare(
+                command="OTB_TF_NSOURCES=2 otbcli_PatchesExtraction "
+                        "-source1.il $DATADIR/s2_20m_stack.jp2 "
+                        "-source1.patchsizex 8 "
+                        "-source1.patchsizey 8 "
+                        "-source1.out $TMPDIR/s2_20m_patches_B.tif "
+                        "-source2.il $DATADIR/s2_stack.jp2 "
+                        "-source2.patchsizex 16 "
+                        "-source2.patchsizey 16 "
+                        "-source2.out $TMPDIR/s2_10m_patches_B.tif "
+                        "-vec $TMPDIR/outvec_B.gpkg "
+                        "-field class "
+                        "-outlabels $TMPDIR/s2_10m_labels_B.tif uint8",
+                to_compare_dict={"$DATADIR/s2_10m_labels_B.tif": "$TMPDIR/s2_10m_labels_B.tif",
+                                 "$DATADIR/s2_10m_patches_B.tif": "$TMPDIR/s2_10m_patches_B.tif",
+                                 "$DATADIR/s2_20m_patches_B.tif": "$TMPDIR/s2_20m_patches_B.tif"}))
+
+    @pytest.mark.order(15)
+    def test_generate_model3(self):
+        self.assertTrue(
+            run_command_and_test_exist(
+                command="python $TMPDIR/otbtf_tuto_repo/01_patch_based_classification/models/create_model3.py "
+                        "$TMPDIR/model3",
+                file_list=["$TMPDIR/model3/saved_model.pb"]))
+
+    @pytest.mark.order(16)
+    def test_model3_train(self):
+        self.assertTrue(
+            run_command_and_test_exist(
+                command="OTB_TF_NSOURCES=2 otbcli_TensorflowModelTrain "
+                        "-training.source1.il $DATADIR/s2_20m_patches_A.tif "
+                        "-training.source1.patchsizex 8 "
+                        "-training.source1.patchsizey 8 "
+                        "-training.source1.placeholder x1 "
+                        "-training.source2.il $DATADIR/s2_10m_patches_A.tif "
+                        "-training.source2.patchsizex 16 "
+                        "-training.source2.patchsizey 16 "
+                        "-training.source2.placeholder x2 "
+                        "-training.source3.il $DATADIR/s2_10m_labels_A.tif "
+                        "-training.source3.patchsizex 1 "
+                        "-training.source3.patchsizey 1 "
+                        "-training.source3.placeholder y "
+                        "-model.dir $TMPDIR/model3 "
+                        "-training.targetnodes optimizer "
+                        "-training.epochs 10 "
+                        "-validation.mode class "
+                        "-validation.source1.il $DATADIR/s2_20m_patches_B.tif "
+                        "-validation.source1.name x1 "
+                        "-validation.source2.il $DATADIR/s2_10m_patches_B.tif "
+                        "-validation.source2.name x2 "
+                        "-validation.source3.il $DATADIR/s2_10m_labels_B.tif "
+                        "-validation.source3.name prediction "
+                        "-model.saveto $TMPDIR/model3/variables/variables",
+                file_list=["$TMPDIR/model3/variables/variables.index"]))
+
+    @pytest.mark.order(17)
+    def test_model3_inference_pb(self):
+        self.assertTrue(
+            run_command_and_compare(
+                command=
+                "OTB_TF_NSOURCES=2 otbcli_TensorflowModelServe "
+                "-source1.il $DATADIR/s2_20m_stack.jp2 "
+                "-source1.rfieldx 8 "
+                "-source1.rfieldy 8 "
+                "-source1.placeholder x1 "
+                "-source2.il $DATADIR/s2_stack.jp2 "
+                "-source2.rfieldx 16 "
+                "-source2.rfieldy 16 "
+                "-source2.placeholder x2 "
+                "-model.dir $TMPDIR/model3 "
+                "-output.names prediction "
+                "-out \"$TMPDIR/classif_model3_pb.tif?&box=2000:2000:500:500&gdal:co:compress=deflate\"",
+                to_compare_dict={"$DATADIR/classif_model3_pb.tif": "$TMPDIR/classif_model3_pb.tif"},
+                tol=INFERENCE_MAE_TOL))
+
+    @pytest.mark.order(18)
+    def test_model3_inference_fcn(self):
+        self.assertTrue(
+            run_command_and_compare(
+                command=
+                "OTB_TF_NSOURCES=2 otbcli_TensorflowModelServe "
+                "-source1.il $DATADIR/s2_20m_stack.jp2 "
+                "-source1.rfieldx 8 "
+                "-source1.rfieldy 8 "
+                "-source1.placeholder x1 "
+                "-source2.il $DATADIR/s2_stack.jp2 "
+                "-source2.rfieldx 16 "
+                "-source2.rfieldy 16 "
+                "-source2.placeholder x2 "
+                "-model.dir $TMPDIR/model3 "
+                "-model.fullyconv on "
+                "-output.names prediction "
+                "-out \"$TMPDIR/classif_model3_fcn.tif?&box=2000:2000:500:500&gdal:co:compress=deflate\"",
+                to_compare_dict={"$DATADIR/classif_model3_fcn.tif": "$TMPDIR/classif_model3_fcn.tif"},
+                tol=INFERENCE_MAE_TOL))
+
+    @pytest.mark.order(19)
+    def test_generate_model4(self):
+        self.assertTrue(
+            run_command_and_test_exist(
+                command="python $TMPDIR/otbtf_tuto_repo/02_semantic_segmentation/models/create_model4.py "
+                        "$TMPDIR/model4",
+                file_list=["$TMPDIR/model4/saved_model.pb"]))
+
+    @pytest.mark.order(20)
+    def test_patches_selection_semseg(self):
+        self.assertTrue(
+            run_command_and_test_exist(
+                command="otbcli_PatchesSelection "
+                        "-in $DATADIR/fake_spot6.jp2 "
+                        "-grid.step 64 "
+                        "-grid.psize 64 "
+                        "-outtrain $TMPDIR/outvec_A_semseg.gpkg "
+                        "-outvalid $TMPDIR/outvec_B_semseg.gpkg",
+                file_list=["$TMPDIR/outvec_A_semseg.gpkg",
+                           "$TMPDIR/outvec_B_semseg.gpkg"]))
+
+    @pytest.mark.order(21)
+    def test_patch_extraction_semseg(self):
+        self.assertTrue(
+            run_command_and_compare(
+                command="OTB_TF_NSOURCES=2 otbcli_PatchesExtraction "
+                        "-source1.il $DATADIR/fake_spot6.jp2 "
+                        "-source1.patchsizex 64 "
+                        "-source1.patchsizey 64 "
+                        "-source1.out \"$TMPDIR/amsterdam_patches_A.tif?&gdal:co:compress=deflate\" "
+                        "-source2.il $TMPDIR/otbtf_tuto_repo/02_semantic_segmentation/"
+                        "amsterdam_dataset/terrain_truth/amsterdam_labelimage.tif "
+                        "-source2.patchsizex 64 "
+                        "-source2.patchsizey 64 "
+                        "-source2.out \"$TMPDIR/amsterdam_labels_A.tif?&gdal:co:compress=deflate\" "
+                        "-vec $TMPDIR/outvec_A_semseg.gpkg "
+                        "-field id ",
+                to_compare_dict={"$DATADIR/amsterdam_labels_A.tif": "$TMPDIR/amsterdam_labels_A.tif",
+                                 "$DATADIR/amsterdam_patches_A.tif": "$TMPDIR/amsterdam_patches_A.tif"}))
+        self.assertTrue(
+            run_command_and_compare(
+                command="OTB_TF_NSOURCES=2 otbcli_PatchesExtraction "
+                        "-source1.il $DATADIR/fake_spot6.jp2 "
+                        "-source1.patchsizex 64 "
+                        "-source1.patchsizey 64 "
+                        "-source1.out \"$TMPDIR/amsterdam_patches_B.tif?&gdal:co:compress=deflate\" "
+                        "-source2.il $TMPDIR/otbtf_tuto_repo/02_semantic_segmentation/"
+                        "amsterdam_dataset/terrain_truth/amsterdam_labelimage.tif "
+                        "-source2.patchsizex 64 "
+                        "-source2.patchsizey 64 "
+                        "-source2.out \"$TMPDIR/amsterdam_labels_B.tif?&gdal:co:compress=deflate\" "
+                        "-vec $TMPDIR/outvec_B_semseg.gpkg "
+                        "-field id ",
+                to_compare_dict={"$DATADIR/amsterdam_labels_B.tif": "$TMPDIR/amsterdam_labels_B.tif",
+                                 "$DATADIR/amsterdam_patches_B.tif": "$TMPDIR/amsterdam_patches_B.tif"}))
+
+    @pytest.mark.order(22)
+    def test_model4_train(self):
+        self.assertTrue(
+            run_command_and_test_exist(
+                command="OTB_TF_NSOURCES=1 otbcli_TensorflowModelTrain "
+                        "-training.source1.il $DATADIR/amsterdam_patches_A.tif "
+                        "-training.source1.patchsizex 64 "
+                        "-training.source1.patchsizey 64 "
+                        "-training.source1.placeholder x "
+                        "-training.source2.il $DATADIR/amsterdam_labels_A.tif "
+                        "-training.source2.patchsizex 64 "
+                        "-training.source2.patchsizey 64 "
+                        "-training.source2.placeholder y "
+                        "-model.dir $TMPDIR/model4 "
+                        "-training.targetnodes optimizer "
+                        "-training.epochs 10 "
+                        "-validation.mode class "
+                        "-validation.source1.il $DATADIR/amsterdam_patches_B.tif "
+                        "-validation.source1.name x "
+                        "-validation.source2.il $DATADIR/amsterdam_labels_B.tif "
+                        "-validation.source2.name prediction "
+                        "-model.saveto $TMPDIR/model4/variables/variables",
+                file_list=["$TMPDIR/model4/variables/variables.index"]))
+
+    @pytest.mark.order(23)
+    def test_model4_inference(self):
+        self.assertTrue(
+            run_command_and_compare(
+                command=
+                "otbcli_TensorflowModelServe "
+                "-source1.il $DATADIR/fake_spot6.jp2 "
+                "-source1.rfieldx 64 "
+                "-source1.rfieldy 64 "
+                "-source1.placeholder x "
+                "-model.dir $TMPDIR/model4 "
+                "-model.fullyconv on "
+                "-output.names prediction_fcn "
+                "-output.efieldx 32 "
+                "-output.efieldy 32 "
+                "-out \"$TMPDIR/classif_model4.tif?&gdal:co:compress=deflate\" uint8",
+                to_compare_dict={"$DATADIR/classif_model4.tif": "$TMPDIR/classif_model4.tif"},
+                tol=INFERENCE_MAE_TOL))
+
+
+if __name__ == '__main__':
+    unittest.main()