diff --git a/aidge_core/aidge_export_aidge/export.py b/aidge_core/aidge_export_aidge/export.py
index b0e859b71e77bb03b95acb6ca75dcf09a8af0722..ac8c2e0b86cc4f7b1009f9a701d6dcb91d248009 100644
--- a/aidge_core/aidge_export_aidge/export.py
+++ b/aidge_core/aidge_export_aidge/export.py
@@ -54,7 +54,7 @@ def export(export_folder: str,
     ### Generating an export for each nodes and dnn file ###
     list_configs = []  # List of headers to include in dnn.cpp to access attribute and parameters
     list_actions = []  # List of string to construct graph
-    set_operator = set()
+    list_operators = [] # List of operator types used (to be made unique latter)
     # Queue of Aidge nodes to explore, guarantee a topological exploration of the graph
     open_nodes = list(graph_view.get_input_nodes())
     # List of Aidge nodes already explored
@@ -82,7 +82,7 @@ def export(export_folder: str,
         open_nodes += list(node.get_children())
 
         if node.type() in supported_operators():
-            set_operator.add(node.type())
+            list_operators.append(node.type())
             op = OPERATORS_REGISTRY[node.type()](node)
 
             # TODO: list_configs and list_actions don't need to be passed by argument
@@ -94,11 +94,13 @@ def export(export_folder: str,
         else:
             raise RuntimeError(f"Operator: {node.type()} is not supported")
         closed_nodes.append(node)
+    list_operators = list(dict.fromkeys(list_operators)) # make unique
+
     # Generate full dnn.cpp
     aidge_core.generate_file(
         export_folder_path / "src/dnn.cpp",
         ROOT_EXPORT / "templates/dnn.jinja",
         headers=list_configs,
-        operators=set_operator,
+        operators=list_operators,
         actions=list_actions,
     )
diff --git a/aidge_core/testing/__init__.py b/aidge_core/testing/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..6966bbb6355c798727870ace37f99193fdda66a2
--- /dev/null
+++ b/aidge_core/testing/__init__.py
@@ -0,0 +1,12 @@
+#
+# Do not add there auto import of submodules.
+#
+# The testing module contains utils and other tools
+# related to tests, possibly reusable by other aidge
+# components unit_tests.
+#
+# Import a specific module explicitly with for instance:
+# import aidge_core.testing.utils
+# or
+# from aidge_core.testing.utils import (....,)
+#
diff --git a/aidge_core/testing/utils/__init__.py b/aidge_core/testing/utils/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..d69be0c5b80c7df229d8a24e6891970b8108fb14
--- /dev/null
+++ b/aidge_core/testing/utils/__init__.py
@@ -0,0 +1,10 @@
+#
+# Should provide some general utility functions for testing.
+# For instance:
+# - filesystem
+# - os dependencies
+# - unit tests setup
+#
+
+from .tree_cache import tree_update_from_cache
+from .tree_utils import tree_move, tree_remove
diff --git a/aidge_core/testing/utils/tree_cache.py b/aidge_core/testing/utils/tree_cache.py
new file mode 100644
index 0000000000000000000000000000000000000000..5b363c7c73ea36636a40c007b24cc244b10303c2
--- /dev/null
+++ b/aidge_core/testing/utils/tree_cache.py
@@ -0,0 +1,145 @@
+"""
+
+Provide tree_update_from_cache(path) method which
+minimize changes in a generated tree when files are
+re-generated but identical.
+
+It takes as argument a generated tree, and optionally a cache path.
+Then it will update both the generated tree and the cache tree
+to take the cache version of the files when identical, or the newly
+generated one otherwise.
+
+This is in particular useful for speeding up iterative compilation
+when generating a source/build system tree.
+
+For instance:
+- first time, one generates a tree of files:
+  - generated: path/{t1,t2,t3}
+- then call tree_update_from_cache("path")
+  - will generate: __cache_path/{t1,t2,t3}
+  - and untouch: path/{t1,t2,t3}
+- second time, re-generate a tree of file:
+  - say generated files are identical: path/{t1,t2,t3}
+- then call tree_update_from_cache("path")
+  - will untouch in cache: __cache_path/{t1,t2,t3}
+  - and reset to previous timestamps files: path/{t1,t2,t3}
+- third time, re-generate again with some changes:
+  - say t1 is identical, t2 content has changed and no t3: path/{t1,t2'}
+- then call tree_update_from_cache("path")
+  - will update t2' and remove t3 in cache: __cache_path/{t1,t2'}
+  - and reset to previous timestamp t1: path/{t1,t2'}
+
+Note that by default the `dir`/__cache_`name` cache path is used
+for a given path `dir`/`name`.
+Though it is also possible to have the cache path inside the generated tree,
+in this case use for instance:
+
+    tree_update_from_cache(path, Path(path) / "__cache_src")
+
+For more evolved scenarii, specialize the provided FileTreeCache class.
+
+"""
+
+
+from pathlib import Path
+import shutil
+import filecmp
+from typing import Optional, Union, List
+
+from .tree_utils import tree_move, tree_remove
+
+
+__all__ = [
+    "FileTreeCache",
+    "tree_update_from_cache",
+]
+
+
+class FileTreeCache():
+    """
+    Class for implementation of the file tree cache.
+    Can be derived to changes for instance default cache name/tmp name prefixes
+    or to specialize for other contexts.
+    """
+    default_cache_prefix = "__cache_"
+    default_tmp_cache_prefix = "__tmp_cache_"
+    default_tmp_prefix = "__tmp_"
+
+    def __init__(self,
+                 src_path: Union[str|Path],
+                 cache_path: Optional[Union[str|Path]] = None
+                 ) -> None:
+        self.src_path = Path(src_path).absolute()
+        self.cache_path = (
+            Path(cache_path).absolute()
+            if cache_path is not None else
+            (self.src_path.parent /
+             f"{self.default_cache_prefix}{self.src_path.name}")
+        )
+        ctx_msg = f"tree_cache: {src_path = }, {cache_path = }"
+        assert self.src_path != self.cache_path, f"src_path and cache_path must differ on {ctx_msg}"
+        assert not self.src_path.is_relative_to(self.cache_path), f"src_path must not be relative to cache_path on {ctx_msg}"
+        self._tmp_path = (
+            self.src_path.parent /
+            f"{self.default_tmp_prefix}{self.src_path.name}")
+        self._tmp_cache_path = (
+            self.src_path.parent /
+            f"{self.default_tmp_cache_prefix}{self.src_path.name}")
+
+    @classmethod
+    def _copytree_or_cache(cls, src_dir: Path, dst_dir: Path, cache_dir: Path, dst_cache_dir: Path) -> None:
+        assert not dst_dir.exists()
+        assert not dst_cache_dir.exists()
+        assert src_dir.is_dir()
+        assert not cache_dir.exists() or cache_dir.is_dir()
+        assert not cache_dir.is_relative_to(src_dir)
+
+        def copy_or_cache(src, dst):
+            base_src = Path(src).relative_to(src_dir)
+            cache_src = cache_dir / base_src
+            base_dst = Path(dst).relative_to(dst_dir)
+            cache_dst = dst_cache_dir / base_dst
+            cache_dst.parent.mkdir(parents=True, exist_ok=True)
+            if cache_src.exists() and filecmp.cmp(str(src), str(cache_src), shallow=False):
+                shutil.copy2(str(cache_src), str(cache_dst))
+                shutil.copy2(str(cache_src), dst)
+            else:
+                shutil.copy2(src, str(cache_dst))
+                shutil.copy2(src, dst)
+        shutil.copytree(str(src_dir), str(dst_dir), copy_function=copy_or_cache)
+
+    def update_from_cache(self) -> None:
+        assert self.src_path.exists(), f"src path must exist before swapping with cache"
+
+        # Move cache path apart first as it may be relative to source path
+        tree_move(self.cache_path, self._tmp_cache_path, ignore_missing=True, exist_ok=True)
+        # Move source path apart before recreating merged source tree
+        tree_move(self.src_path, self._tmp_path, exist_ok=True)
+
+        # Manage the source/cache merge to the dst/dst_cahe with a variant of
+        # copytree.
+        self._copytree_or_cache(
+            src_dir=self._tmp_path,
+            dst_dir=self.src_path,
+            cache_dir=self._tmp_cache_path,
+            dst_cache_dir=self.cache_path,
+        )
+
+        # Remove tmp source path
+        tree_remove(self._tmp_path)
+        # Note that the tmp cache path may not exist
+        tree_remove(self._tmp_cache_path, ignore_missing=True)
+
+
+def tree_update_from_cache(
+        src_path: Union[str|Path],
+        cache_path: Optional[Union[str|Path]] = None) -> None:
+    """
+    Update from cache the current generation of a tree from the
+    older generations, preserving file stamps when files contents are identical.
+
+    :param src_path: str or Path object to the generated tree
+    :param cache_path: optional str or Path object to the cache path,
+    or defaults to: `cache_path = src_path.parent / f"__cache_{src_path.name}"`
+    """
+    FileTreeCache(src_path, cache_path).update_from_cache()
diff --git a/aidge_core/testing/utils/tree_utils.py b/aidge_core/testing/utils/tree_utils.py
new file mode 100644
index 0000000000000000000000000000000000000000..3a6b2aad88e16075ed64bee03ba8e8fa550376e2
--- /dev/null
+++ b/aidge_core/testing/utils/tree_utils.py
@@ -0,0 +1,65 @@
+"""
+
+Provide utility function for file trees manipulations.
+
+"""
+
+import shutil
+from pathlib import Path
+from typing import Union, Optional
+
+
+__all__ = [
+    "tree_move",
+    "tree_remove",
+]
+
+
+def tree_remove(
+        path: Union[str|Path],
+        ignore_missing: bool = False,
+) -> None:
+    """
+    Remove the full tree at path.
+    Optionally ignore if the path does not exist when ignore_missing is True.
+
+    :param path: str or Path object to the directory path
+    :param ignore_missing: if True will return early is path does not exists
+    """
+    path = Path(path)
+    ctx_msg = f"tree_remove: : {path = }"
+    assert ignore_missing or path.exists(), f"path must exists when ignore_missing is False on {ctx_msg}"
+    if ignore_missing and not path.exists():
+        return
+    shutil.rmtree(path)
+
+
+def tree_move(
+        src_path: Union[str|Path],
+        dst_path: Union[str|Path],
+        ignore_missing: bool = False,
+        exist_ok: bool = False,
+) -> None:
+    """
+    Move the whole src_path file tree to dst_path.
+    Optionally does nothing if the src path does not exists and ignore_missing is True.
+    Optionally the full dst_path will be removed first when exists_ok is True.
+
+    :param src_path: str or Path object to the source directory path
+    :param dst_path: str or Path object to the new path name for the source directory
+    :param ignore_missing: if True will return early is src_path does not exists
+    :param exist_ok: if True will first erase the new path name if it exists
+    """
+    src_path = Path(src_path)
+    dst_path = Path(dst_path)
+    ctx_msg = f"tree_move: : {src_path = }, {dst_path = }"
+    assert ignore_missing or src_path.exists(), f"src_path must exists when ignore_missing is False on {ctx_msg}"
+    assert exist_ok or not dst_path.exists(), f"dst_path must not exists when exist_ok is False on {ctx_msg}"
+    assert src_path != dst_path, f"paths must not be identical on {ctx_msg}"
+    assert not dst_path.is_relative_to(src_path), f"dst_path must not be relative to src_path on {ctx_msg}"
+    assert not src_path.is_relative_to(dst_path), f"src_path must not be relative to dst_path on {ctx_msg}"
+    if ignore_missing and not src_path.exists():
+        return
+    if exist_ok and dst_path.exists():
+        shutil.rmtree(dst_path)
+    shutil.move(src_path, dst_path)
diff --git a/aidge_core/unit_tests/test_export.py b/aidge_core/unit_tests/test_export.py
index 5d2e700a86925d1455cdee83e7d40cd891e72ba6..57a15586873fae10beb428d0cd3cb41267a45d2f 100644
--- a/aidge_core/unit_tests/test_export.py
+++ b/aidge_core/unit_tests/test_export.py
@@ -8,8 +8,6 @@ http://www.eclipse.org/legal/epl-2.0.
 SPDX-License-Identifier: EPL-2.0
 """
 
-import aidge_core
-from aidge_core.utils import run_command
 import unittest
 import os
 import pathlib
@@ -18,6 +16,10 @@ import subprocess
 import sys
 
 
+import aidge_core
+from aidge_core.utils import run_command
+from aidge_core.testing.utils import tree_update_from_cache, tree_move, tree_remove
+
 def initFiller(model):
     # Initialize parameters (weights and biases)
     for node in model.get_nodes():
@@ -26,6 +28,8 @@ def initFiller(model):
             value = prod_op.get_output(0)
             value.set_backend("cpu")
             tuple_out = node.output(0)[0]
+            # Force seed before filler for reproducibility
+            aidge_core.random.Generator.set_seed(0)
             # No conv in current network
             if tuple_out[0].type() == "Conv" and tuple_out[1] == 1:
                 # Conv weight
@@ -43,22 +47,6 @@ def initFiller(model):
                 pass
 
 
-def clean_dir(dir: pathlib.Path) -> None:
-    if not dir.is_dir():
-        print(f"Error : directory {dir} doesn't exist. Exiting clean_dir().")
-        return
-    for filename in os.listdir(dir):
-        file_path = os.path.join(dir, filename)
-        try:
-            if os.path.isfile(file_path) or os.path.islink(file_path):
-                os.unlink(file_path)
-            elif os.path.isdir(file_path):
-                shutil.rmtree(file_path)
-        except Exception as e:
-            print(f"Failed to delete {file_path}. Reason: {e}")
-    return
-
-
 class test_export(unittest.TestCase):
     """Test aidge export"""
 
@@ -66,6 +54,10 @@ class test_export(unittest.TestCase):
         self.EXPORT_PATH: pathlib.Path = pathlib.Path("dummy_export")
         self.BUILD_DIR: pathlib.Path = self.EXPORT_PATH / "build"
         self.INSTALL_DIR: pathlib.Path = (self.EXPORT_PATH / "install").absolute()
+        self.TMP_BUILD_DIR: pathlib.Path = (
+            self.EXPORT_PATH.parent /
+            f"__tmp_{self.EXPORT_PATH.name}_build"
+        )
 
     def tearDown(self):
         pass
@@ -76,28 +68,40 @@ class test_export(unittest.TestCase):
         model = aidge_core.sequential(
             [
                 aidge_core.FC(
-                    in_channels=32 * 32 * 3, out_channels=512, name="InputNode"
+                    in_channels=32 * 32 * 3, out_channels=64, name="InputNode"
                 ),
                 aidge_core.ReLU(name="Relu0"),
-                aidge_core.FC(in_channels=512, out_channels=256, name="FC1"),
+                aidge_core.FC(in_channels=64, out_channels=32, name="FC1"),
                 aidge_core.ReLU(name="Relu1"),
-                aidge_core.FC(in_channels=256, out_channels=128, name="FC2"),
+                aidge_core.FC(in_channels=32, out_channels=16, name="FC2"),
                 aidge_core.ReLU(name="Relu2"),
-                aidge_core.FC(in_channels=128, out_channels=10, name="OutputNode"),
+                aidge_core.FC(in_channels=16, out_channels=10, name="OutputNode"),
             ]
         )
 
         initFiller(model)
 
+        # Preserve previously generated build if present
+        tree_move(self.BUILD_DIR, self.TMP_BUILD_DIR, ignore_missing=True, exist_ok=True)
+        # Clean install dir
+        tree_remove(self.INSTALL_DIR, ignore_missing=True)
+
         # Export model
         aidge_core.export(self.EXPORT_PATH, model)
-
-        self.assertTrue(
-            self.EXPORT_PATH.is_dir(), "Export folder has not been generated"
+        self.assertTrue(self.EXPORT_PATH.is_dir(), "Export folder has not been generated")
+        # Add other source files
+        shutil.copyfile(pathlib.Path(__file__).parent / "static/main.cpp", self.EXPORT_PATH / "main.cpp")
+
+        # Use cache if any, put cache inside export dir
+        # such that cleaning export dir also cleans the cache
+        tree_update_from_cache(
+            self.EXPORT_PATH,
+            cache_path=self.EXPORT_PATH / "__cache_export"
         )
-        os.makedirs(self.BUILD_DIR, exist_ok=True)
-        clean_dir(self.BUILD_DIR)  # if build dir existed already ensure its emptyness
-        clean_dir(self.INSTALL_DIR)
+
+        # Move back preserved build dir if any and ensure build dir exists
+        tree_move(self.TMP_BUILD_DIR, self.BUILD_DIR, ignore_missing=True)
+        self.BUILD_DIR.mkdir(exist_ok=True)
 
         # Test compilation of export
         search_path = (
@@ -106,11 +110,6 @@ class test_export(unittest.TestCase):
             else os.environ["AIDGE_INSTALL"]
         )
 
-        shutil.copyfile(
-            pathlib.Path(__file__).parent / "static/main.cpp",
-            self.EXPORT_PATH / "main.cpp",
-        )
-
         ##########################
         # CMAKE EXPORT
         try: