diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,10 @@
 # Revision history for ogma-core
 
+## [1.9.0] - 2025-08-06
+
+* Version bump 1.9.0 (#284).
+* Extend ROS backend to generate testing package (#280).
+
 ## [1.8.0] - 2025-07-13
 
 * Version bump 1.8.0 (#275).
diff --git a/ogma-core.cabal b/ogma-core.cabal
--- a/ogma-core.cabal
+++ b/ogma-core.cabal
@@ -32,7 +32,7 @@
 build-type:          Simple
 
 name:                ogma-core
-version:             1.8.0
+version:             1.9.0
 homepage:            https://github.com/nasa/ogma
 bug-reports:         https://github.com/nasa/ogma/issues
 license:             OtherLicense
@@ -69,11 +69,15 @@
                      templates/copilot-cfs/fsw/src/copilot_cfs.h
                      templates/copilot-cfs/fsw/src/copilot_cfs_events.h
                      templates/ros/Dockerfile
+                     templates/ros/screenrc
                      templates/ros/copilot/CMakeLists.txt
                      templates/ros/copilot/src/copilot_logger.cpp
                      templates/ros/copilot/src/copilot_monitor.cpp
                      templates/ros/copilot/src/Copilot.hs
                      templates/ros/copilot/package.xml
+                     templates/ros/test_requirements/CMakeLists.txt
+                     templates/ros/test_requirements/src/test_requirements.cpp
+                     templates/ros/test_requirements/package.xml
                      templates/diagram/Copilot.hs
                      templates/fprime/CMakeLists.txt
                      templates/fprime/Copilot.cpp
@@ -148,16 +152,16 @@
     , process                 >= 1.6      && < 1.7
     , text                    >= 1.2.3.1  && < 2.2
 
-    , ogma-extra              >= 1.8.0 && < 1.9
-    , ogma-language-c         >= 1.8.0 && < 1.9
-    , ogma-language-copilot   >= 1.8.0 && < 1.9
-    , ogma-language-csv       >= 1.8.0 && < 1.9
-    , ogma-language-jsonspec  >= 1.8.0 && < 1.9
-    , ogma-language-lustre    >= 1.8.0 && < 1.9
-    , ogma-language-smv       >= 1.8.0 && < 1.9
-    , ogma-language-xlsx      >= 1.8.0 && < 1.9
-    , ogma-language-xmlspec   >= 1.8.0 && < 1.9
-    , ogma-spec               >= 1.8.0 && < 1.9
+    , ogma-extra              >= 1.9.0 && < 1.10
+    , ogma-language-c         >= 1.9.0 && < 1.10
+    , ogma-language-copilot   >= 1.9.0 && < 1.10
+    , ogma-language-csv       >= 1.9.0 && < 1.10
+    , ogma-language-jsonspec  >= 1.9.0 && < 1.10
+    , ogma-language-lustre    >= 1.9.0 && < 1.10
+    , ogma-language-smv       >= 1.9.0 && < 1.10
+    , ogma-language-xlsx      >= 1.9.0 && < 1.10
+    , ogma-language-xmlspec   >= 1.9.0 && < 1.10
+    , ogma-spec               >= 1.9.0 && < 1.10
 
   hs-source-dirs:
     src
diff --git a/src/Command/ROSApp.hs b/src/Command/ROSApp.hs
--- a/src/Command/ROSApp.hs
+++ b/src/Command/ROSApp.hs
@@ -44,6 +44,7 @@
 module Command.ROSApp
     ( command
     , CommandOptions(..)
+    , Node(Node)
     , ErrorCode
     )
   where
@@ -119,10 +120,18 @@
                      (map (\x -> (x, Nothing)))
                      rs
 
-    let appData   = AppData variables monitors' copilotM
+    let appData =
+          AppData variables monitors' copilotM testingAdditionalApps testingVars
+
         variables = mapMaybe (variableMap varDB) varNames
         monitors' = mapMaybe (monitorMap varDB) monitors
 
+        testingVars
+          | null testingLimitedVars
+          = variables
+          | otherwise
+          = filter (\x -> varDeclName x `elem` testingLimitedVars) variables
+
     return appData
 
   where
@@ -145,6 +154,9 @@
     processSpec spec' expr' fp' =
       Command.Standalone.commandLogic expr' fp' "copilot" [] exprT spec'
 
+    testingAdditionalApps = commandTestingApps options
+    testingLimitedVars    = commandTestingVars options
+
 -- ** Argument processing
 
 -- | Options used to customize the conversion of specifications to ROS
@@ -174,6 +186,10 @@
   , commandExtraVars   :: Maybe FilePath -- ^ File containing additional
                                          -- variables to make available to the
                                          -- template.
+  , commandTestingApps :: [Node]         -- ^ Additional applications to turn
+                                         -- on during testing.
+  , commandTestingVars :: [String]       -- ^ Limited list of variables to use
+                                         -- for testing.
   }
 
 -- | Return the variable information needed to generate declarations
@@ -192,7 +208,7 @@
   let typeMsg' = fromMaybe
                    (topicType topicDef)
                    (typeFromType <$> findType varDB varName "ros/message" "C")
-  return $ VarDecl varName typeVar' mid typeMsg'
+  return $ VarDecl varName typeVar' mid typeMsg' (randomBaseType typeVar')
 
 -- | Return the monitor information needed to generate declarations and
 -- publishers for the given monitor info, and variable database.
@@ -212,6 +228,7 @@
     , varDeclType    :: String
     , varDeclId      :: String
     , varDeclMsgType :: String
+    , varDeclRandom  :: String
     }
   deriving Generic
 
@@ -228,12 +245,40 @@
 
 instance ToJSON Monitor
 
+-- | A package-qualified ROS 2 node name.
+data Node = Node
+    { nodePackage :: String
+    , nodeName    :: String
+    }
+  deriving Generic
+
+instance ToJSON Node
+
 -- | Data that may be relevant to generate a ROS application.
 data AppData = AppData
-  { variables :: [VarDecl]
-  , monitors  :: [Monitor]
-  , copilot   :: Maybe Command.Standalone.AppData
+  { variables        :: [VarDecl]
+  , monitors         :: [Monitor]
+  , copilot          :: Maybe Command.Standalone.AppData
+  , testingApps      :: [Node]
+  , testingVariables :: [VarDecl]
   }
   deriving (Generic)
 
 instance ToJSON AppData
+
+-- | Name of the function to be used to generate random values of a given type.
+randomBaseType :: String -- ^ Type to generate random values of.
+               -> String
+randomBaseType ty = case ty of
+  "bool"     -> "randomBool"
+  "uint8_t"  -> "randomInt"
+  "uint16_t" -> "randomInt"
+  "uint32_t" -> "randomInt"
+  "uint64_t" -> "randomInt"
+  "int8_t"   -> "randomInt"
+  "int16_t"  -> "randomInt"
+  "int32_t"  -> "randomInt"
+  "int64_t"  -> "randomInt"
+  "float"    -> "randomFloat"
+  "double"   -> "randomFloat"
+  def        -> def
diff --git a/templates/ros/Dockerfile b/templates/ros/Dockerfile
--- a/templates/ros/Dockerfile
+++ b/templates/ros/Dockerfile
@@ -6,6 +6,7 @@
 
 RUN mkdir -p ${PACKAGE_PATH}/src/
 ADD copilot ${PACKAGE_PATH}/src/copilot
+ADD test_requirements ${PACKAGE_PATH}/src/test_requirements
 USER root
 RUN chown -R ${USER} ${PACKAGE_PATH}
 USER ${USER}
@@ -13,4 +14,11 @@
 SHELL ["/bin/bash", "-c"]
 WORKDIR ${PACKAGE_PATH}
 RUN source /opt/spaceros/install/setup.bash && \
-    colcon build
+    colcon build --packages-select copilot           --install-base /opt/spaceros/install/ && \
+    colcon build --packages-select test_requirements --install-base /opt/spaceros/install/
+
+ADD screenrc /home/spaceros-user/.screenrc
+USER root
+ADD screenrc /root/.screenrc
+RUN chown -R spaceros-user /home/spaceros-user/.screenrc
+USER spaceros-user
diff --git a/templates/ros/screenrc b/templates/ros/screenrc
new file mode 100644
--- /dev/null
+++ b/templates/ros/screenrc
@@ -0,0 +1,19 @@
+deflogin off
+startup_message off
+
+chdir /demo
+screen -t "Copilot RV" bash -c 'bash --init-file <(echo "source /home/spaceros-user/spaceros/install/setup.bash; set -x; ros2 run copilot copilot")'
+
+{{#testingApps}}
+split
+focus
+screen -t "{{nodeName}}" bash -c 'bash --init-file <(echo "source /home/spaceros-user/spaceros/install/setup.bash; set -x; ros2 run {{nodePackage}} {{nodeName}}")'
+
+{{/testingApps}}
+split
+focus
+screen -t "Requirements testing" bash -c 'sleep 2; bash --init-file <( echo "source /home/spaceros-user/spaceros/install/setup.bash; set -x; ros2 run test_requirements test_requirements")'
+
+split
+focus
+screen -t "<interactive shell>" bash -c 'bash --init-file <(echo "echo This is an interactive shell. Send data to topics with ros2 topic pub \<type\> \<data\>; source /home/spaceros-user/spaceros/install/setup.bash")'
diff --git a/templates/ros/test_requirements/CMakeLists.txt b/templates/ros/test_requirements/CMakeLists.txt
new file mode 100644
--- /dev/null
+++ b/templates/ros/test_requirements/CMakeLists.txt
@@ -0,0 +1,26 @@
+cmake_minimum_required(VERSION 3.8)
+project(test_requirements)
+
+if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
+  add_compile_options(-Wall -Wextra -Wpedantic)
+endif()
+
+# find dependencies
+find_package(ament_cmake REQUIRED)
+find_package(rclcpp REQUIRED)
+find_package(std_msgs REQUIRED)
+
+# Uncomment to enable compiling the requirement tests
+add_executable(test_requirements src/test_requirements.cpp)
+ament_target_dependencies(test_requirements rclcpp std_msgs)
+
+install(TARGETS
+  test_requirements
+  DESTINATION lib/${PROJECT_NAME})
+
+if(BUILD_TESTING)
+  find_package(ament_lint_auto REQUIRED)
+  ament_lint_auto_find_test_dependencies()
+endif()
+
+ament_package()
diff --git a/templates/ros/test_requirements/package.xml b/templates/ros/test_requirements/package.xml
new file mode 100644
--- /dev/null
+++ b/templates/ros/test_requirements/package.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0"?>
+<?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
+<package format="3">
+  <name>test_requirements</name>
+  <version>0.0.0</version>
+  <description>TODO: Package description</description>
+  <maintainer email="root@todo.todo">root</maintainer>
+  <license>TODO: License declaration</license>
+
+  <buildtool_depend>ament_cmake</buildtool_depend>
+  <depend>rclcpp</depend>
+  <depend>std_msgs</depend>
+
+  <test_depend>ament_lint_auto</test_depend>
+  <test_depend>ament_lint_common</test_depend>
+
+  <export>
+    <build_type>ament_cmake</build_type>
+  </export>
+</package>
diff --git a/templates/ros/test_requirements/src/test_requirements.cpp b/templates/ros/test_requirements/src/test_requirements.cpp
new file mode 100644
--- /dev/null
+++ b/templates/ros/test_requirements/src/test_requirements.cpp
@@ -0,0 +1,192 @@
+#include <functional>
+#include <memory>
+
+#include "gtest/gtest.h"
+
+#include "rclcpp/rclcpp.hpp"
+
+#include "std_msgs/msg/bool.hpp"
+#include "std_msgs/msg/empty.hpp"
+#include "std_msgs/msg/u_int8.hpp"
+#include "std_msgs/msg/u_int16.hpp"
+#include "std_msgs/msg/u_int32.hpp"
+#include "std_msgs/msg/u_int64.hpp"
+#include "std_msgs/msg/int8.hpp"
+#include "std_msgs/msg/int16.hpp"
+#include "std_msgs/msg/int32.hpp"
+#include "std_msgs/msg/int64.hpp"
+#include "std_msgs/msg/float32.hpp"
+#include "std_msgs/msg/float64.hpp"
+#include <cstdint>
+
+using std::placeholders::_1;
+
+class RequirementsTest : public rclcpp::Node {
+  public:
+    RequirementsTest() : Node("requirementstest") {
+
+      declare_parameter("testing_seed", 0); // defaults to 0
+      declare_parameter("testing_deadline", 2); // defaults to 2 secs
+
+
+{{#testingVariables}}
+      {{varDeclName}}_publisher_ = this->create_publisher<{{varDeclMsgType}}>(
+        "{{varDeclId}}", 10);
+
+{{/testingVariables}}
+
+{{#monitors}}
+      {{monitorName}}_subscription_ = this->create_subscription<std_msgs::msg::Empty>(
+        "copilot/{{monitorName}}", 10,
+        std::bind(&RequirementsTest::{{monitorName}}_callback, this, _1));
+
+{{/monitors}}
+
+      get_parameter("testing_seed", initial_seed);
+      get_parameter("testing_deadline", deadline);
+
+      std::srand((unsigned int)this->initial_seed);
+
+      this->seed = this->initial_seed;
+      this->max_tests = calculate_num_tests();
+
+      rclcpp::Duration update_period = rclcpp::Duration::from_seconds(1);
+      timerInit = rclcpp::create_timer(this->get_node_base_interface(),
+                                       this->get_node_timers_interface(),
+                                       this->get_node_clock_interface()->get_clock(),
+                                       update_period,
+                                       std::bind(&RequirementsTest::tests_init, this)
+                                       );
+    }
+
+  private:
+
+
+{{#monitors}}
+    bool violation_{{monitorName}} = false;
+
+{{/monitors}}
+    bool violations = false;
+
+
+{{#testingVariables}}
+    rclcpp::Publisher<{{varDeclMsgType}}>::SharedPtr {{varDeclName}}_publisher_;
+
+{{/testingVariables}}
+{{#monitors}}
+    void {{monitorName}}_callback(const std_msgs::msg::Empty::SharedPtr msg) {
+        this->violation_{{monitorName}} = true;
+        this->violations = true;
+    }
+{{/monitors}}
+
+{{#monitors}}
+    rclcpp::Subscription<std_msgs::msg::Empty>::SharedPtr {{monitorName}}_subscription_;
+
+{{/monitors}}
+
+
+    int initial_seed; // To be configured using a parameter.
+    int seed;         // To be configured using a parameter.
+    int deadline;     // To be configured using a parameter.
+
+    int max_tests;
+    int num_test = 0;
+
+    // Calculate the number of tests to be executed
+    int calculate_num_tests() {
+       return abs(std::rand());
+    }
+
+    rclcpp::TimerBase::SharedPtr timerResult;
+    rclcpp::TimerBase::SharedPtr timerInit;
+
+    void tests_init () {
+        timerInit->cancel();
+        tests_step_send();
+    }
+
+    void tests_step_send () {
+
+{{#testingVariables}}
+       {{varDeclType}} {{varDeclName}}_data = {{varDeclRandom}}();
+       auto {{varDeclName}}_data_msg = {{varDeclMsgType}}();
+       {{varDeclName}}_data_msg.data = {{varDeclName}}_data;
+       {{varDeclName}}_publisher_->publish({{varDeclName}}_data_msg);
+
+{{/testingVariables}}
+
+
+        rclcpp::Duration update_period = rclcpp::Duration::from_seconds(deadline);
+        timerResult = rclcpp::create_timer(this->get_node_base_interface(),
+                                           this->get_node_timers_interface(),
+                                           this->get_node_clock_interface()->get_clock(),
+                                           update_period,
+                                           std::bind(&RequirementsTest::tests_step_result, this)
+                                           );
+    }
+
+    void tests_step_result () {
+        timerResult->cancel();
+
+{{#monitors}}
+        if (this->violation_{{monitorName}}) {
+            this->publish_violation("{{monitorName}}");
+        }
+
+{{/monitors}}
+
+       this->num_test++;
+
+       // Stop if out of steps or there have been violations
+       if ((this->num_test >= this->max_tests) || violations) {
+         // Terminate using the gtest mechanism to indicate the result
+         if (violations) {
+           RCLCPP_INFO(this->get_logger(), "Tests failed");
+           // FAIL();
+         } else {
+           RCLCPP_INFO(this->get_logger(), "Tests succeeded");
+           // SUCCEED();
+         }
+         rclcpp::shutdown();
+       } else {
+         tests_step_send();
+       }
+    }
+
+    float randomFloat() {
+       int numerator = rand();
+       int denominator = rand();
+
+       // Ensure that we do not divide by zero.
+       if (denominator == 0) {
+           denominator = 1;
+       }
+
+       return (float)numerator / (float)denominator;
+    }
+
+    int randomInt() {
+       return rand();
+    }
+
+    bool randomBool() {
+       return rand() & 1;
+    }
+
+    void delay(int time) {
+       rclcpp::sleep_for(std::chrono::seconds(time));
+    }
+
+    void publish_violation (const char* requirement) {
+        RCLCPP_INFO(this->get_logger(), "Requirement violation. Req: %s; Seed: %d; Step: %d\\n",
+            requirement, this->initial_seed, this->num_test);
+    }
+};
+
+int main(int argc, char* argv[]) {
+  rclcpp::init(argc, argv);
+  rclcpp::spin(std::make_shared<RequirementsTest>());
+  rclcpp::shutdown();
+  return 0;
+}
