diff --git a/modcc/cprinter.cpp b/modcc/cprinter.cpp
index d6b1e5370083637c3d34d9a12d40c6535856b31d..3d4b01d4a4fe608da52f83a26699a5e206604edf 100644
--- a/modcc/cprinter.cpp
+++ b/modcc/cprinter.cpp
@@ -305,12 +305,9 @@ CPrinter::CPrinter(Module &m, bool o)
     //////////////////////////////////////////////
 
     auto proctest = [] (procedureKind k) {
-        return
-            k == procedureKind::normal
-                 || k == procedureKind::api
-                 || k == procedureKind::net_receive;
+        return is_in(k, {procedureKind::normal, procedureKind::api, procedureKind::net_receive});
     };
-    for(auto &var : m.symbols()) {
+    for(auto const& var: m.symbols()) {
         auto isproc = var.second->kind()==symbolKind::procedure;
         if(isproc )
         {
@@ -461,10 +458,6 @@ void CPrinter::visit(BlockExpression *e) {
             }
         }
         if(names.size()>0) {
-            //for(auto it=names.begin(); it!=names.end(); ++it) {
-            //    text_.add_gutter() << "value_type " << *it;
-            //    text_.end_line("{0};");
-            //}
             text_.add_gutter() << "value_type " << *(names.begin());
             for(auto it=names.begin()+1; it!=names.end(); ++it) {
                 text_ << ", " << *it;
@@ -498,8 +491,9 @@ void CPrinter::visit(IfExpression *e) {
     text_ << "}";
 }
 
+// NOTE: net_receive() is classified as a ProcedureExpression
 void CPrinter::visit(ProcedureExpression *e) {
-    // ------------- print prototype ------------- //
+    // print prototype
     text_.add_gutter() << "void " << e->name() << "(int i_";
     for(auto& arg : e->args()) {
         text_ << ", value_type " << arg->is_argument()->name();
@@ -518,19 +512,18 @@ void CPrinter::visit(ProcedureExpression *e) {
             e->location());
     }
 
+    // print body
     increase_indentation();
-
     e->body()->accept(this);
 
-    // ------------- close up ------------- //
+    // close the function body
     decrease_indentation();
     text_.add_line("}");
     text_.add_line();
-    return;
 }
 
 void CPrinter::visit(APIMethod *e) {
-    // ------------- print prototype ------------- //
+    // print prototype
     text_.add_gutter() << "void " << e->name() << "() override {";
     text_.end_line();
 
@@ -566,7 +559,7 @@ void CPrinter::visit(APIMethod *e) {
             }
         }
 
-        // ------------- get loop dimensions ------------- //
+        // get loop dimensions
         text_.add_line("int n_ = node_index_.size();");
 
         // hand off printing of loops to optimized or unoptimized backend
@@ -578,14 +571,12 @@ void CPrinter::visit(APIMethod *e) {
         }
     }
 
-    // ------------- close up ------------- //
+    // close up the loop body
     text_.add_line("}");
     text_.add_line();
 }
 
 void CPrinter::print_APIMethod_unoptimized(APIMethod* e) {
-    //text_.add_line("START_PROFILE");
-
     // there can not be more than 1 instance of a density channel per grid point,
     // so we can assert that aliasing will not occur.
     if(optimize_) text_.add_line("#pragma ivdep");
diff --git a/modcc/cprinter.hpp b/modcc/cprinter.hpp
index e669c25f02e71da10e9f4622ad07651bbd558e36..ef47977c05fe13e1b6f3206bcffecf344855d355 100644
--- a/modcc/cprinter.hpp
+++ b/modcc/cprinter.hpp
@@ -44,6 +44,9 @@ public:
     void decrease_indentation(){
         text_.decrease_indentation();
     }
+    void clear_text() {
+        text_.clear();
+    }
 private:
 
     void print_APIMethod_optimized(APIMethod* e);
diff --git a/modcc/cudaprinter.cpp b/modcc/cudaprinter.cpp
index e5103ddf3a4a78dbd0fd0db48bb8f62f7d3caa9d..175781a203df3351ccc62051813c24e0a1e8262b 100644
--- a/modcc/cudaprinter.cpp
+++ b/modcc/cudaprinter.cpp
@@ -1,5 +1,6 @@
 #include <algorithm>
 
+#include "cprinter.hpp" // needed for printing net_receive method
 #include "cudaprinter.hpp"
 #include "lexer.hpp"
 
@@ -114,27 +115,11 @@ CUDAPrinter::CUDAPrinter(Module &m, bool o)
         text_.decrease_indentation();
         text_.add_line("}");
         text_.add_line();
-        /*
-        text_.add_line("__device__");
-        text_.add_line("inline double atomicSub(double* address, double val) {");
-        text_.increase_indentation();
-        text_.add_line("return atomicAdd(address, -val);");
-        text_.decrease_indentation();
-        text_.add_line("}");
-        text_.add_line();
-        text_.add_line("__device__");
-        text_.add_line("inline float atomicSub(float* address, float val) {");
-        text_.increase_indentation();
-        text_.add_line("return atomicAdd(address, -val);");
-        text_.decrease_indentation();
-        text_.add_line("}");
-        text_.add_line();
-        */
 
         // forward declarations of procedures
         for(auto const &var : m.symbols()) {
-            if(   var.second->kind()==symbolKind::procedure
-            && var.second->is_procedure()->kind() == procedureKind::normal)
+            if( var.second->kind()==symbolKind::procedure &&
+                var.second->is_procedure()->kind() == procedureKind::normal)
             {
                 print_procedure_prototype(var.second->is_procedure());
                 text_.end_line(";");
@@ -144,11 +129,10 @@ CUDAPrinter::CUDAPrinter(Module &m, bool o)
 
         // print stubs that call API method kernels that are defined in the
         // kernels::name namespace
-        auto proctest = [] (procedureKind k) {return k == procedureKind::normal
-                                                  || k == procedureKind::api;   };
         for(auto const &var : m.symbols()) {
             if (var.second->kind()==symbolKind::procedure &&
-                proctest(var.second->is_procedure()->kind()))
+                is_in(var.second->is_procedure()->kind(),
+                      {procedureKind::normal, procedureKind::api}))
             {
                 var.second->accept(this);
             }
@@ -428,11 +412,9 @@ CUDAPrinter::CUDAPrinter(Module &m, bool o)
 
     //////////////////////////////////////////////
     //////////////////////////////////////////////
-
-    auto proctest = [] (procedureKind k) {return k == procedureKind::api;};
     for(auto const &var : m.symbols()) {
-        if(   var.second->kind()==symbolKind::procedure
-        && proctest(var.second->is_procedure()->kind()))
+        if( var.second->kind()==symbolKind::procedure && 
+            var.second->is_procedure()->kind()==procedureKind::api)
         {
             auto proc = var.second->is_api_method();
             auto name = proc->name();
@@ -450,6 +432,29 @@ CUDAPrinter::CUDAPrinter(Module &m, bool o)
             text_.add_line("}");
             text_.add_line();
         }
+        else if( var.second->kind()==symbolKind::procedure &&
+                 var.second->is_procedure()->kind()==procedureKind::net_receive)
+        {
+            auto proc = var.second->is_procedure();
+            auto name = proc->name();
+            text_.add_line("void " + name + "(int i_, value_type weight) {");
+            text_.increase_indentation();
+
+            // Print the body of the net_receive block.
+            // Use the same body as would be generated with the cprinter.
+            // This is not omptimal, because each read and write will require
+            // a copy between host and device memory, so we will need a
+            // GPU-specific implementation
+            auto cprinter = CPrinter(*module_);
+            cprinter.clear_text();
+            cprinter.set_gutter(text_.get_gutter());
+            proc->body()->accept(&cprinter);
+            text_ << cprinter.text();
+
+            text_.decrease_indentation();
+            text_.add_line("}");
+            text_.add_line();
+        }
     }
 
     //////////////////////////////////////////////
@@ -648,28 +653,26 @@ void CUDAPrinter::visit(ProcedureExpression *e) {
             e->location());
     }
 
-    // ------------- print prototype ------------- //
+    // print prototype
     print_procedure_prototype(e);
     text_.end_line(" {");
 
-    // ------------- print body ------------- //
+    // print body
     increase_indentation();
 
     text_.add_line("using value_type = T;");
-    text_.add_line("using iarray = I;");
     text_.add_line();
 
     e->body()->accept(this);
 
-    // ------------- close up ------------- //
+    // close up
     decrease_indentation();
     text_.add_line("}");
     text_.add_line();
-    return;
 }
 
 void CUDAPrinter::visit(APIMethod *e) {
-    // ------------- print prototype ------------- //
+    // print prototype
     text_.add_gutter() << "template <typename T, typename I>\n";
     text_.add_line(       "__global__");
     text_.add_gutter() << "void " << e->name()
@@ -702,7 +705,8 @@ void CUDAPrinter::visit(APIMethod *e) {
     text_.add_line("}");
 
     decrease_indentation();
-    text_.add_line("}\n");
+    text_.add_line("}");
+    text_.add_line();
 }
 
 void CUDAPrinter::print_APIMethod_body(APIMethod* e) {
diff --git a/modcc/textbuffer.cpp b/modcc/textbuffer.cpp
index 5c8af1df5df85a1f72e241c0b3f461892a65edaa..658d20c4714e698967034de295327c20bb082702 100644
--- a/modcc/textbuffer.cpp
+++ b/modcc/textbuffer.cpp
@@ -28,6 +28,9 @@ void TextBuffer::set_gutter(int width) {
     indent_ = width;
     gutter_ = std::string(indent_, ' ');
 }
+int TextBuffer::get_gutter() {
+    return indent_;
+}
 
 void TextBuffer::increase_indentation() {
     indent_ += indentation_width_;
@@ -47,3 +50,7 @@ void TextBuffer::decrease_indentation() {
 std::stringstream& TextBuffer::text() {
     return text_;
 }
+
+void TextBuffer::clear() {
+    text_.str("");
+}
diff --git a/modcc/textbuffer.hpp b/modcc/textbuffer.hpp
index cdb3fad3daacece05c39f88a7b5e13f0c95aa779..55b6da678daa5ebebc5bc373bf91864984a4686e 100644
--- a/modcc/textbuffer.hpp
+++ b/modcc/textbuffer.hpp
@@ -18,11 +18,14 @@ public:
     std::string str() const;
 
     void set_gutter(int width);
+    int get_gutter();
 
     void increase_indentation();
     void decrease_indentation();
     std::stringstream &text();
 
+    void clear();
+
 private:
 
     int indent_ = 0;
diff --git a/src/backends/fvm_multicore.hpp b/src/backends/fvm_multicore.hpp
index 73a0d98874909d78cbb9fbcc3f59a0005f8be8fd..71a0ba0d06f370ed09f5ce6513fce16223477bc9 100644
--- a/src/backends/fvm_multicore.hpp
+++ b/src/backends/fvm_multicore.hpp
@@ -132,7 +132,7 @@ struct backend {
     static bool has_mechanism(const std::string& name) { return mech_map_.count(name)>0; }
 
     static std::string name() {
-        return "multicore";
+        return "cpu";
     }
 
 private:
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index 5fd3493290cff53f87d5c1b5a1d6a620d4b16bc4..7036ee30616a0941bb0b1cf95e84dfff1a5d16e1 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -1,8 +1,5 @@
 # google test framework
 add_library(gtest gtest-all.cpp)
-# tests look for gtest.h here
-include_directories(${CMAKE_CURRENT_SOURCE_DIR})
-
 
 # Unit tests
 add_subdirectory(unit)
diff --git a/tests/global_communication/mpi_listener.hpp b/tests/global_communication/mpi_listener.hpp
index e33aaebb5cea2168baead70e3418567c10bcdd6e..18d245e4dd2cb3f3752e4fe22a342a28bfa305bd 100644
--- a/tests/global_communication/mpi_listener.hpp
+++ b/tests/global_communication/mpi_listener.hpp
@@ -1,12 +1,12 @@
 #pragma once
 
 #include <cstdio>
-#include <ostream>
+#include <fstream>
 #include <stdexcept>
 
 #include <communication/global_policy.hpp>
 
-#include "gtest.h"
+#include "../gtest.h"
 
 /// A specialized listener desinged for printing test results with MPI.
 ///
diff --git a/tests/global_communication/test.cpp b/tests/global_communication/test.cpp
index 1ddb6d41baf5d84cefc5b7f4e542a7cf79d2598c..fae2502972c9f8f1025112ca15b069861d4476a8 100644
--- a/tests/global_communication/test.cpp
+++ b/tests/global_communication/test.cpp
@@ -3,7 +3,7 @@
 #include <numeric>
 #include <vector>
 
-#include "gtest.h"
+#include "../gtest.h"
 
 #include "mpi_listener.hpp"
 
diff --git a/tests/global_communication/test_communicator.cpp b/tests/global_communication/test_communicator.cpp
index f79d4e2d6e42b382bc4b54e16d29187ea531b7c0..b6bcc03404322b6a7b32eda1afaa83b459056982 100644
--- a/tests/global_communication/test_communicator.cpp
+++ b/tests/global_communication/test_communicator.cpp
@@ -1,4 +1,4 @@
-#include "gtest.h"
+#include "../gtest.h"
 
 #include <cstdio>
 #include <fstream>
diff --git a/tests/global_communication/test_exporter_spike_file.cpp b/tests/global_communication/test_exporter_spike_file.cpp
index 4a85b0784801df6f91c72db70b44b69b36337f2b..027a315bf05aeac72055d1fe64deb64bf9fbc911 100644
--- a/tests/global_communication/test_exporter_spike_file.cpp
+++ b/tests/global_communication/test_exporter_spike_file.cpp
@@ -1,4 +1,4 @@
-#include "gtest.h"
+#include "../gtest.h"
 
 #include <cstdio>
 #include <fstream>
diff --git a/tests/global_communication/test_mpi_gather_all.cpp b/tests/global_communication/test_mpi_gather_all.cpp
index b5ecf73fa7088357e8bdbcc851d44f6f190d7b7b..f67df15ac159f40df6741ca8edfa0dff68f8e415 100644
--- a/tests/global_communication/test_mpi_gather_all.cpp
+++ b/tests/global_communication/test_mpi_gather_all.cpp
@@ -1,7 +1,7 @@
-#include "gtest.h"
-
 #ifdef WITH_MPI
 
+#include "../gtest.h"
+
 #include <cstring>
 #include <vector>
 
diff --git a/tests/modcc/test.hpp b/tests/modcc/test.hpp
index d3de72506d2ef679227fcc71f0d76aab141b3a9c..45dced6442094dd73ad0aac80b51d743c43c2750 100644
--- a/tests/modcc/test.hpp
+++ b/tests/modcc/test.hpp
@@ -1,6 +1,6 @@
 #pragma once
 
-#include <gtest.h>
+#include "../gtest.h"
 
 #include "parser.hpp"
 #include "modccutil.hpp"
diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt
index 2dff23105bf24ae85cbca57d9db48b8f1220db03..75c46c46b55db64f158cc85d739d67762698d835 100644
--- a/tests/unit/CMakeLists.txt
+++ b/tests/unit/CMakeLists.txt
@@ -1,4 +1,5 @@
 set(TEST_CUDA_SOURCES
+    test_cell_group.cu
     test_matrix.cu
     test_vector.cu
 
diff --git a/tests/unit/test_cell_group.cu b/tests/unit/test_cell_group.cu
new file mode 100644
index 0000000000000000000000000000000000000000..16bc124dec87108f97071c7f3e13c193c02402aa
--- /dev/null
+++ b/tests/unit/test_cell_group.cu
@@ -0,0 +1,36 @@
+#include "../gtest.h"
+
+#include <cell_group.hpp>
+#include <common_types.hpp>
+#include <fvm_multicell.hpp>
+#include <util/rangeutil.hpp>
+
+#include "../test_common_cells.hpp"
+
+using namespace nest::mc;
+
+using fvm_cell =
+    fvm::fvm_multicell<nest::mc::gpu::backend>;
+
+nest::mc::cell make_cell() {
+    using namespace nest::mc;
+
+    cell c = make_cell_ball_and_stick();
+
+    c.add_detector({0, 0}, 0);
+    c.segment(1)->set_compartments(101);
+
+    return c;
+}
+
+TEST(cell_group, test)
+{
+    using cell_group_type = cell_group<fvm_cell>;
+    auto group = cell_group_type{0, util::singleton_view(make_cell())};
+
+    group.advance(50, 0.01);
+
+    // the model is expected to generate 4 spikes as a result of the
+    // fixed stimulus over 50 ms
+    EXPECT_EQ(4u, group.spikes().size());
+}
diff --git a/tests/unit/test_filter.cpp b/tests/unit/test_filter.cpp
index 0d357edd3c53fc62c1618f4173935bec27179af6..d8ca4e64e6ca465abcd1486ab3e3275ae1555845 100644
--- a/tests/unit/test_filter.cpp
+++ b/tests/unit/test_filter.cpp
@@ -1,5 +1,3 @@
-#include "gtest.h"
-
 #include <cstring>
 #include <list>
 
@@ -8,6 +6,8 @@
 #include <util/filter.hpp>
 #include <util/transform.hpp>
 
+#include "../gtest.h"
+
 #include "common.hpp"
 
 using namespace nest::mc;
diff --git a/tests/validation/CMakeLists.txt b/tests/validation/CMakeLists.txt
index a5d217b27a0e54e85d1cc744a810bfdc629a3fd4..d9e16533bc8dcc082211a08b85522a5c248361e5 100644
--- a/tests/validation/CMakeLists.txt
+++ b/tests/validation/CMakeLists.txt
@@ -13,27 +13,38 @@ set(VALIDATION_SOURCES
     validate.cpp
 )
 
+set(VALIDATION_CUDA_SOURCES
+    # unit tests
+    validate_soma.cu
+    validate_ball_and_stick.cu
+    validate_synapses.cu
+
+    # support code
+    validation_data.cpp
+    trace_analysis.cpp
+
+    # unit test driver
+    validate.cpp
+)
+
 if(VALIDATION_DATA_DIR)
     add_definitions("-DDATADIR=\"${VALIDATION_DATA_DIR}\"")
 endif()
-add_executable(validate.exe ${VALIDATION_SOURCES} ${HEADERS})
 
+add_executable(validate.exe ${VALIDATION_SOURCES})
 set(TARGETS validate.exe)
 
-foreach(target ${TARGETS})
-    target_link_libraries(${target} LINK_PUBLIC nestmc gtest)
+if(WITH_CUDA)
+    cuda_add_executable(validate_cuda.exe ${VALIDATION_CUDA_SOURCES})
+    list(APPEND TARGETS validate_cuda.exe)
+    target_link_libraries(validate_cuda.exe LINK_PUBLIC gpu)
+endif()
 
-    if(WITH_TBB)
-        target_link_libraries(${target} LINK_PUBLIC ${TBB_LIBRARIES})
-    endif()
 
-    if(WITH_CUDA)
-        target_link_libraries(${target} LINK_PUBLIC ${CUDA_LIBRARIES})
-    endif()
+foreach(target ${TARGETS})
+    target_link_libraries(${target} LINK_PUBLIC nestmc gtest)
 
-    if(WITH_UNWIND)
-        target_link_libraries(${target} LINK_PUBLIC ${UNWIND_LIBRARIES})
-    endif()
+    target_link_libraries(${target} LINK_PUBLIC ${EXTERNAL_LIBRARIES})
 
     if(WITH_MPI)
         target_link_libraries(${target} LINK_PUBLIC ${MPI_C_LIBRARIES})
diff --git a/tests/validation/convergence_test.hpp b/tests/validation/convergence_test.hpp
index e536a0f1408f53ccfd6b7c4e1f5329e7c83572ee..0561e7d76a9088b011b4bbd79f11e273f891b389 100644
--- a/tests/validation/convergence_test.hpp
+++ b/tests/validation/convergence_test.hpp
@@ -2,10 +2,11 @@
 
 #include <util/filter.hpp>
 #include <util/rangeutil.hpp>
+#include <cell.hpp>
 
 #include <json/json.hpp>
 
-#include "gtest.h"
+#include "../gtest.h"
 
 #include "trace_analysis.hpp"
 #include "validation_data.hpp"
diff --git a/tests/validation/tinyopt.hpp b/tests/validation/tinyopt.hpp
index 85a2e4a7dae008819827f79319fd32badd99b01e..90eb145f312511460786fa7f5a004604c5c54b8e 100644
--- a/tests/validation/tinyopt.hpp
+++ b/tests/validation/tinyopt.hpp
@@ -6,7 +6,7 @@
 #include <stdexcept>
 #include <string>
 
-#include "gtest.h"
+#include "../gtest.h"
 
 #include <communication/global_policy.hpp>
 
diff --git a/tests/validation/trace_analysis.cpp b/tests/validation/trace_analysis.cpp
index 78ef5ada3ff39e8155aa9db97181274fb0fef118..b83d41a2ca9ce76d79d63dd84744294366a56c18 100644
--- a/tests/validation/trace_analysis.cpp
+++ b/tests/validation/trace_analysis.cpp
@@ -4,7 +4,7 @@
 
 #include <json/json.hpp>
 
-#include "gtest.h"
+#include "../gtest.h"
 
 #include <math.hpp>
 #include <simple_sampler.hpp>
diff --git a/tests/validation/trace_analysis.hpp b/tests/validation/trace_analysis.hpp
index 493a15f7bc205b90589e22a379363bddc29276c5..8b0976237bc1790dc13141ee86c625cf196fc4d4 100644
--- a/tests/validation/trace_analysis.hpp
+++ b/tests/validation/trace_analysis.hpp
@@ -2,7 +2,7 @@
 
 #include <vector>
 
-#include "gtest.h"
+#include "../gtest.h"
 
 #include <simple_sampler.hpp>
 #include <math.hpp>
diff --git a/tests/validation/validate.cpp b/tests/validation/validate.cpp
index 9f071aa2ed719d0de39bafb6009ecd5415618651..a2f2f8322cc9c63e03eddf41957521e5bbdffb99 100644
--- a/tests/validation/validate.cpp
+++ b/tests/validation/validate.cpp
@@ -4,10 +4,10 @@
 #include <string>
 #include <exception>
 
-#include "gtest.h"
-
 #include <communication/global_policy.hpp>
 
+#include "../gtest.h"
+
 #include "tinyopt.hpp"
 #include "validation_data.hpp"
 
diff --git a/tests/validation/validate_ball_and_stick.cpp b/tests/validation/validate_ball_and_stick.cpp
index 01c9bbaaf2d500444e117f4940a17b6f2bcc912c..0e31bcaad33723829317a02d183ba9f40bb20f76 100644
--- a/tests/validation/validate_ball_and_stick.cpp
+++ b/tests/validation/validate_ball_and_stick.cpp
@@ -1,159 +1,22 @@
-#include <json/json.hpp>
-
-#include <cell.hpp>
-#include <common_types.hpp>
-#include <fvm_multicell.hpp>
-#include <model.hpp>
-#include <recipe.hpp>
-#include <simple_sampler.hpp>
-#include <util/path.hpp>
-
 #include "../gtest.h"
+#include "validate_ball_and_stick.hpp"
 
-#include "../test_common_cells.hpp"
-#include "convergence_test.hpp"
-#include "trace_analysis.hpp"
-#include "validation_data.hpp"
-
-using namespace nest::mc;
-
-template <
-    typename lowered_cell,
-    typename SamplerInfoSeq
->
-void run_ncomp_convergence_test(
-    const char* model_name,
-    const util::path& ref_data_path,
-    const cell& c,
-    SamplerInfoSeq& samplers,
-    float t_end=100.f)
-{
-    auto max_ncomp = g_trace_io.max_ncomp();
-    auto dt = g_trace_io.min_dt();
-
-    nlohmann::json meta = {
-        {"name", "membrane voltage"},
-        {"model", model_name},
-        {"dt", dt},
-        {"sim", "nestmc"},
-        {"units", "mV"}
-    };
-
-    auto exclude = stimulus_ends(c);
-
-    convergence_test_runner<int> R("ncomp", samplers, meta);
-    R.load_reference_data(ref_data_path);
-
-    for (int ncomp = 10; ncomp<max_ncomp; ncomp*=2) {
-        for (auto& seg: c.segments()) {
-            if (!seg->is_soma()) {
-                seg->set_compartments(ncomp);
-            }
-        }
-        model<lowered_cell> m(singleton_recipe{c});
-
-        R.run(m, ncomp, t_end, dt, exclude);
-    }
-    R.report();
-    R.assert_all_convergence();
-}
-
-TEST(ball_and_taper, neuron_ref) {
-    using lowered_cell = fvm::fvm_multicell<multicore::backend>;
-
-    cell c = make_cell_ball_and_stick();
-    add_common_voltage_probes(c);
+#include <fvm_multicell.hpp>
 
-    float sample_dt = 0.025f;
-    sampler_info samplers[] = {
-        {"soma.mid", {0u, 0u}, simple_sampler(sample_dt)},
-        {"dend.mid", {0u, 1u}, simple_sampler(sample_dt)},
-        {"dend.end", {0u, 2u}, simple_sampler(sample_dt)}
-    };
+using lowered_cell = nest::mc::fvm::fvm_multicell<nest::mc::multicore::backend>;
 
-    run_ncomp_convergence_test<lowered_cell>(
-        "ball_and_stick",
-        "neuron_ball_and_stick.json",
-        c,
-        samplers);
+TEST(ball_and_stick, neuron_ref) {
+    validate_ball_and_stick<lowered_cell>();
 }
 
 TEST(ball_and_3stick, neuron_ref) {
-    using lowered_cell = fvm::fvm_multicell<multicore::backend>;
-
-    cell c = make_cell_ball_and_3stick();
-    add_common_voltage_probes(c);
-
-    float sample_dt = 0.025f;
-    sampler_info samplers[] = {
-        {"soma.mid",  {0u, 0u}, simple_sampler(sample_dt)},
-        {"dend1.mid", {0u, 1u}, simple_sampler(sample_dt)},
-        {"dend1.end", {0u, 2u}, simple_sampler(sample_dt)},
-        {"dend2.mid", {0u, 3u}, simple_sampler(sample_dt)},
-        {"dend2.end", {0u, 4u}, simple_sampler(sample_dt)},
-        {"dend3.mid", {0u, 5u}, simple_sampler(sample_dt)},
-        {"dend3.end", {0u, 6u}, simple_sampler(sample_dt)}
-    };
-
-    run_ncomp_convergence_test<lowered_cell>(
-        "ball_and_3stick",
-        "neuron_ball_and_3stick.json",
-        c,
-        samplers);
+    validate_ball_and_3stick<lowered_cell>();
 }
 
 TEST(rallpack1, numeric_ref) {
-    using lowered_cell = fvm::fvm_multicell<multicore::backend>;
-
-    cell c = make_cell_simple_cable();
-
-    // three probes: left end, 30% along, right end.
-    c.add_probe({{1, 0.0}, probeKind::membrane_voltage});
-    c.add_probe({{1, 0.3}, probeKind::membrane_voltage});
-    c.add_probe({{1, 1.0}, probeKind::membrane_voltage});
-
-    float sample_dt = 0.025f;
-    sampler_info samplers[] = {
-        {"cable.x0.0", {0u, 0u}, simple_sampler(sample_dt)},
-        {"cable.x0.3", {0u, 1u}, simple_sampler(sample_dt)},
-        {"cable.x1.0", {0u, 2u}, simple_sampler(sample_dt)},
-    };
-
-    run_ncomp_convergence_test<lowered_cell>(
-        "rallpack1",
-        "numeric_rallpack1.json",
-        c,
-        samplers,
-        250.f);
+    validate_rallpack1<lowered_cell>();
 }
 
 TEST(ball_and_squiggle, neuron_ref) {
-    using lowered_cell = fvm::fvm_multicell<multicore::backend>;
-
-    cell c = make_cell_ball_and_squiggle();
-    add_common_voltage_probes(c);
-
-    float sample_dt = 0.025f;
-    sampler_info samplers[] = {
-        {"soma.mid", {0u, 0u}, simple_sampler(sample_dt)},
-        {"dend.mid", {0u, 1u}, simple_sampler(sample_dt)},
-        {"dend.end", {0u, 2u}, simple_sampler(sample_dt)}
-    };
-
-#if 0
-    // *temporarily* disabled: compartment division policy will
-    // be moved into backend policy classes.
-
-    run_ncomp_convergence_test<lowered_cell_div<div_compartment_sampler>>(
-        "ball_and_squiggle_sampler",
-        "neuron_ball_and_squiggle.json",
-        c,
-        samplers);
-#endif
-
-    run_ncomp_convergence_test<lowered_cell>(
-        "ball_and_squiggle_integrator",
-        "neuron_ball_and_squiggle.json",
-        c,
-        samplers);
+    validate_ball_and_squiggle<lowered_cell>();
 }
diff --git a/tests/validation/validate_ball_and_stick.cu b/tests/validation/validate_ball_and_stick.cu
new file mode 100644
index 0000000000000000000000000000000000000000..b753b6b5d744c822aef7e65fc41e88b7dbeeeafb
--- /dev/null
+++ b/tests/validation/validate_ball_and_stick.cu
@@ -0,0 +1,22 @@
+#include "../gtest.h"
+#include "validate_ball_and_stick.hpp"
+
+#include <fvm_multicell.hpp>
+
+using lowered_cell = nest::mc::fvm::fvm_multicell<nest::mc::gpu::backend>;
+
+TEST(ball_and_stick, neuron_ref) {
+    validate_ball_and_stick<lowered_cell>();
+}
+
+TEST(ball_and_3stick, neuron_ref) {
+    validate_ball_and_3stick<lowered_cell>();
+}
+
+TEST(rallpack1, numeric_ref) {
+    validate_rallpack1<lowered_cell>();
+}
+
+TEST(ball_and_squiggle, neuron_ref) {
+    validate_ball_and_squiggle<lowered_cell>();
+}
diff --git a/tests/validation/validate_ball_and_stick.hpp b/tests/validation/validate_ball_and_stick.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..bed16d4d59663d7d3bcd8bf069358a386a53b2bd
--- /dev/null
+++ b/tests/validation/validate_ball_and_stick.hpp
@@ -0,0 +1,165 @@
+#include <json/json.hpp>
+
+#include <cell.hpp>
+#include <common_types.hpp>
+#include <fvm_multicell.hpp>
+#include <model.hpp>
+#include <recipe.hpp>
+#include <simple_sampler.hpp>
+#include <util/path.hpp>
+
+#include "../gtest.h"
+
+#include "../test_common_cells.hpp"
+#include "convergence_test.hpp"
+#include "trace_analysis.hpp"
+#include "validation_data.hpp"
+
+template <
+    typename LoweredCell,
+    typename SamplerInfoSeq
+>
+void run_ncomp_convergence_test(
+    const char* model_name,
+    const nest::mc::util::path& ref_data_path,
+    const nest::mc::cell& c,
+    SamplerInfoSeq& samplers,
+    float t_end=100.f)
+{
+    using namespace nest::mc;
+
+    auto max_ncomp = g_trace_io.max_ncomp();
+    auto dt = g_trace_io.min_dt();
+
+    nlohmann::json meta = {
+        {"name", "membrane voltage"},
+        {"model", model_name},
+        {"dt", dt},
+        {"sim", "nestmc"},
+        {"units", "mV"},
+        {"backend", LoweredCell::backend::name()}
+    };
+
+    auto exclude = stimulus_ends(c);
+
+    convergence_test_runner<int> runner("ncomp", samplers, meta);
+    runner.load_reference_data(ref_data_path);
+
+    for (int ncomp = 10; ncomp<max_ncomp; ncomp*=2) {
+        for (auto& seg: c.segments()) {
+            if (!seg->is_soma()) {
+                seg->set_compartments(ncomp);
+            }
+        }
+        model<LoweredCell> m(singleton_recipe{c});
+
+        runner.run(m, ncomp, t_end, dt, exclude);
+    }
+    runner.report();
+    runner.assert_all_convergence();
+}
+
+
+template <typename LoweredCell>
+void validate_ball_and_stick() {
+    using namespace nest::mc;
+
+    cell c = make_cell_ball_and_stick();
+    add_common_voltage_probes(c);
+
+    float sample_dt = 0.025f;
+    sampler_info samplers[] = {
+        {"soma.mid", {0u, 0u}, simple_sampler(sample_dt)},
+        {"dend.mid", {0u, 1u}, simple_sampler(sample_dt)},
+        {"dend.end", {0u, 2u}, simple_sampler(sample_dt)}
+    };
+
+    run_ncomp_convergence_test<LoweredCell>(
+        "ball_and_stick",
+        "neuron_ball_and_stick.json",
+        c,
+        samplers);
+}
+
+template <typename LoweredCell>
+void validate_ball_and_3stick() {
+    using namespace nest::mc;
+
+    cell c = make_cell_ball_and_3stick();
+    add_common_voltage_probes(c);
+
+    float sample_dt = 0.025f;
+    sampler_info samplers[] = {
+        {"soma.mid",  {0u, 0u}, simple_sampler(sample_dt)},
+        {"dend1.mid", {0u, 1u}, simple_sampler(sample_dt)},
+        {"dend1.end", {0u, 2u}, simple_sampler(sample_dt)},
+        {"dend2.mid", {0u, 3u}, simple_sampler(sample_dt)},
+        {"dend2.end", {0u, 4u}, simple_sampler(sample_dt)},
+        {"dend3.mid", {0u, 5u}, simple_sampler(sample_dt)},
+        {"dend3.end", {0u, 6u}, simple_sampler(sample_dt)}
+    };
+
+    run_ncomp_convergence_test<LoweredCell>(
+        "ball_and_3stick",
+        "neuron_ball_and_3stick.json",
+        c,
+        samplers);
+}
+
+template <typename LoweredCell>
+void validate_rallpack1() {
+    using namespace nest::mc;
+
+    cell c = make_cell_simple_cable();
+
+    // three probes: left end, 30% along, right end.
+    c.add_probe({{1, 0.0}, probeKind::membrane_voltage});
+    c.add_probe({{1, 0.3}, probeKind::membrane_voltage});
+    c.add_probe({{1, 1.0}, probeKind::membrane_voltage});
+
+    float sample_dt = 0.025f;
+    sampler_info samplers[] = {
+        {"cable.x0.0", {0u, 0u}, simple_sampler(sample_dt)},
+        {"cable.x0.3", {0u, 1u}, simple_sampler(sample_dt)},
+        {"cable.x1.0", {0u, 2u}, simple_sampler(sample_dt)},
+    };
+
+    run_ncomp_convergence_test<LoweredCell>(
+        "rallpack1",
+        "numeric_rallpack1.json",
+        c,
+        samplers,
+        250.f);
+}
+
+template <typename LoweredCell>
+void validate_ball_and_squiggle() {
+    using namespace nest::mc;
+
+    cell c = make_cell_ball_and_squiggle();
+    add_common_voltage_probes(c);
+
+    float sample_dt = 0.025f;
+    sampler_info samplers[] = {
+        {"soma.mid", {0u, 0u}, simple_sampler(sample_dt)},
+        {"dend.mid", {0u, 1u}, simple_sampler(sample_dt)},
+        {"dend.end", {0u, 2u}, simple_sampler(sample_dt)}
+    };
+
+#if 0
+    // *temporarily* disabled: compartment division policy will
+    // be moved into backend policy classes.
+
+    run_ncomp_convergence_test<lowered_cell_div<div_compartment_sampler>>(
+        "ball_and_squiggle_sampler",
+        "neuron_ball_and_squiggle.json",
+        c,
+        samplers);
+#endif
+
+    run_ncomp_convergence_test<LoweredCell>(
+        "ball_and_squiggle_integrator",
+        "neuron_ball_and_squiggle.json",
+        c,
+        samplers);
+}
diff --git a/tests/validation/validate_compartment_policy.cpp b/tests/validation/validate_compartment_policy.cpp
index e03f800c27b340efd211d5e79ddc9f33d68ce66b..4848ea12a14c9da2bce2dff0fd28ff4f042b4bd5 100644
--- a/tests/validation/validate_compartment_policy.cpp
+++ b/tests/validation/validate_compartment_policy.cpp
@@ -11,7 +11,7 @@
 #include <simple_sampler.hpp>
 #include <util/rangeutil.hpp>
 
-#include "gtest.h"
+#include "../gtest.h"
 
 #include "../test_common_cells.hpp"
 #include "../test_util.hpp"
diff --git a/tests/validation/validate_soma.cpp b/tests/validation/validate_soma.cpp
index f7dcf4dc8b5e74d7fce3b331ffa7ad7878cdcf95..e4e58e7b665c05b0e2afdc99f76e0c4c545e905a 100644
--- a/tests/validation/validate_soma.cpp
+++ b/tests/validation/validate_soma.cpp
@@ -1,61 +1,9 @@
-#include <fstream>
-#include <utility>
-
-#include <json/json.hpp>
-
-#include <common_types.hpp>
-#include <cell.hpp>
-#include <fvm_multicell.hpp>
-#include <model.hpp>
-#include <recipe.hpp>
-#include <simple_sampler.hpp>
-#include <util/rangeutil.hpp>
+#include "validate_soma.hpp"
 
 #include "../gtest.h"
 
-#include "../test_common_cells.hpp"
-#include "convergence_test.hpp"
-#include "trace_analysis.hpp"
-#include "validation_data.hpp"
-
-using namespace nest::mc;
+using lowered_cell = nest::mc::fvm::fvm_multicell<nest::mc::multicore::backend>;
 
 TEST(soma, numeric_ref) {
-    using lowered_cell = fvm::fvm_multicell<multicore::backend>;
-
-    cell c = make_cell_soma_only();
-    add_common_voltage_probes(c);
-    model<lowered_cell> m(singleton_recipe{c});
-
-    float sample_dt = .025f;
-    sampler_info samplers[] = {{"soma.mid", {0u, 0u}, simple_sampler(sample_dt)}};
-
-    nlohmann::json meta = {
-        {"name", "membrane voltage"},
-        {"model", "soma"},
-        {"sim", "nestmc"},
-        {"units", "mV"}
-    };
-
-    convergence_test_runner<float> R("dt", samplers, meta);
-    R.load_reference_data("numeric_soma.json");
-
-    float t_end = 100.f;
-
-    // use dt = 0.05, 0.025, 0.01, 0.005, 0.0025,  ...
-    double max_oo_dt = std::round(1.0/g_trace_io.min_dt());
-    for (double base = 100; ; base *= 10) {
-        for (double multiple: {5., 2.5, 1.}) {
-            double oo_dt = base/multiple;
-            if (oo_dt>max_oo_dt) goto end;
-
-            m.reset();
-            float dt = float(1./oo_dt);
-            R.run(m, dt, t_end, dt);
-        }
-    }
-end:
-
-    R.report();
-    R.assert_all_convergence();
+    validate_soma<lowered_cell>();
 }
diff --git a/tests/validation/validate_soma.cu b/tests/validation/validate_soma.cu
new file mode 100644
index 0000000000000000000000000000000000000000..35355ab93d66715fcf79ce27d064d2cdd119df07
--- /dev/null
+++ b/tests/validation/validate_soma.cu
@@ -0,0 +1,9 @@
+#include "validate_soma.hpp"
+
+#include "../gtest.h"
+
+using lowered_cell = nest::mc::fvm::fvm_multicell<nest::mc::gpu::backend>;
+
+TEST(soma, numeric_ref) {
+    validate_soma<lowered_cell>();
+}
diff --git a/tests/validation/validate_soma.hpp b/tests/validation/validate_soma.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..52bc7d5e087ed575372aa524812d9b23ad38fc33
--- /dev/null
+++ b/tests/validation/validate_soma.hpp
@@ -0,0 +1,56 @@
+#include <json/json.hpp>
+
+#include <common_types.hpp>
+#include <cell.hpp>
+#include <fvm_multicell.hpp>
+#include <model.hpp>
+#include <recipe.hpp>
+#include <simple_sampler.hpp>
+#include <util/rangeutil.hpp>
+
+#include "../test_common_cells.hpp"
+#include "convergence_test.hpp"
+#include "trace_analysis.hpp"
+#include "validation_data.hpp"
+
+template <typename LoweredCell>
+void validate_soma() {
+    using namespace nest::mc;
+
+    cell c = make_cell_soma_only();
+    add_common_voltage_probes(c);
+    model<LoweredCell> model(singleton_recipe{c});
+
+    float sample_dt = .025f;
+    sampler_info samplers[] = {{"soma.mid", {0u, 0u}, simple_sampler(sample_dt)}};
+
+    nlohmann::json meta = {
+        {"name", "membrane voltage"},
+        {"model", "soma"},
+        {"sim", "nestmc"},
+        {"units", "mV"},
+        {"backend", LoweredCell::backend::name()}
+    };
+
+    convergence_test_runner<float> runner("dt", samplers, meta);
+    runner.load_reference_data("numeric_soma.json");
+
+    float t_end = 100.f;
+
+    // use dt = 0.05, 0.025, 0.01, 0.005, 0.0025,  ...
+    double max_oo_dt = std::round(1.0/g_trace_io.min_dt());
+    for (double base = 100; ; base *= 10) {
+        for (double multiple: {5., 2.5, 1.}) {
+            double oo_dt = base/multiple;
+            if (oo_dt>max_oo_dt) goto end;
+
+            model.reset();
+            float dt = float(1./oo_dt);
+            runner.run(model, dt, t_end, dt);
+        }
+    }
+end:
+
+    runner.report();
+    runner.assert_all_convergence();
+}
diff --git a/tests/validation/validate_synapses.cpp b/tests/validation/validate_synapses.cpp
index e90fd494d03420edbff7e58b9e71a5253be740f5..23413cc91c44a8c890ee9635e6deada92db95e10 100644
--- a/tests/validation/validate_synapses.cpp
+++ b/tests/validation/validate_synapses.cpp
@@ -1,81 +1,16 @@
-#include <json/json.hpp>
-
-#include <cell.hpp>
-#include <cell_group.hpp>
 #include <fvm_multicell.hpp>
-#include <model.hpp>
-#include <recipe.hpp>
-#include <simple_sampler.hpp>
-#include <util/path.hpp>
 
 #include "../gtest.h"
+#include "validate_synapses.hpp"
 
-#include "../test_common_cells.hpp"
-#include "convergence_test.hpp"
-#include "trace_analysis.hpp"
-#include "validation_data.hpp"
-
-using namespace nest::mc;
-
-void run_synapse_test(
-    const char* syn_type,
-    const util::path& ref_data_path,
-    float t_end=70.f,
-    float dt=0.001)
-{
-    using lowered_cell = fvm::fvm_multicell<multicore::backend>;
-
-    auto max_ncomp = g_trace_io.max_ncomp();
-    nlohmann::json meta = {
-        {"name", "membrane voltage"},
-        {"model", syn_type},
-        {"sim", "nestmc"},
-        {"units", "mV"}
-    };
-
-    cell c = make_cell_ball_and_stick(false); // no stimuli
-    parameter_list syn_default(syn_type);
-    c.add_synapse({1, 0.5}, syn_default);
-    add_common_voltage_probes(c);
-
-    // injected spike events
-    postsynaptic_spike_event<float> synthetic_events[] = {
-        {{0u, 0u}, 10.0, 0.04},
-        {{0u, 0u}, 20.0, 0.04},
-        {{0u, 0u}, 40.0, 0.04}
-    };
-
-    // exclude points of discontinuity from linf analysis
-    std::vector<float> exclude = {10.f, 20.f, 40.f};
-
-    float sample_dt = 0.025f;
-    sampler_info samplers[] = {
-        {"soma.mid", {0u, 0u}, simple_sampler(sample_dt)},
-        {"dend.mid", {0u, 1u}, simple_sampler(sample_dt)},
-        {"dend.end", {0u, 2u}, simple_sampler(sample_dt)}
-    };
-
-    convergence_test_runner<int> R("ncomp", samplers, meta);
-    R.load_reference_data(ref_data_path);
-
-    for (int ncomp = 10; ncomp<max_ncomp; ncomp*=2) {
-        c.cable(1)->set_compartments(ncomp);
-        model<lowered_cell> m(singleton_recipe{c});
-        m.group(0).enqueue_events(synthetic_events);
-
-        R.run(m, ncomp, t_end, dt, exclude);
-    }
-    R.report();
-    R.assert_all_convergence();
-}
+using lowered_cell = nest::mc::fvm::fvm_multicell<nest::mc::multicore::backend>;
 
 TEST(simple_synapse, expsyn_neuron_ref) {
     SCOPED_TRACE("expsyn");
-    run_synapse_test("expsyn", "neuron_simple_exp_synapse.json");
+    run_synapse_test<lowered_cell>("expsyn", "neuron_simple_exp_synapse.json");
 }
 
 TEST(simple_synapse, exp2syn_neuron_ref) {
     SCOPED_TRACE("exp2syn");
-    run_synapse_test("exp2syn", "neuron_simple_exp2_synapse.json");
+    run_synapse_test<lowered_cell>("exp2syn", "neuron_simple_exp2_synapse.json");
 }
-
diff --git a/tests/validation/validate_synapses.cu b/tests/validation/validate_synapses.cu
new file mode 100644
index 0000000000000000000000000000000000000000..0dedd584268f77011908aa593886a5534965a482
--- /dev/null
+++ b/tests/validation/validate_synapses.cu
@@ -0,0 +1,16 @@
+#include <fvm_multicell.hpp>
+
+#include "../gtest.h"
+#include "validate_synapses.hpp"
+
+using lowered_cell = nest::mc::fvm::fvm_multicell<nest::mc::gpu::backend>;
+
+TEST(simple_synapse, expsyn_neuron_ref) {
+    SCOPED_TRACE("expsyn");
+    run_synapse_test<lowered_cell>("expsyn", "neuron_simple_exp_synapse.json");
+}
+
+TEST(simple_synapse, exp2syn_neuron_ref) {
+    SCOPED_TRACE("exp2syn");
+    run_synapse_test<lowered_cell>("exp2syn", "neuron_simple_exp2_synapse.json");
+}
diff --git a/tests/validation/validate_synapses.hpp b/tests/validation/validate_synapses.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..a6ee35e90f271866563a69d33d8ff62d3320cb95
--- /dev/null
+++ b/tests/validation/validate_synapses.hpp
@@ -0,0 +1,70 @@
+#include <json/json.hpp>
+
+#include <cell.hpp>
+#include <cell_group.hpp>
+#include <fvm_multicell.hpp>
+#include <model.hpp>
+#include <recipe.hpp>
+#include <simple_sampler.hpp>
+#include <util/path.hpp>
+
+#include "../gtest.h"
+
+#include "../test_common_cells.hpp"
+#include "convergence_test.hpp"
+#include "trace_analysis.hpp"
+#include "validation_data.hpp"
+
+template <typename LoweredCell>
+void run_synapse_test(
+    const char* syn_type,
+    const nest::mc::util::path& ref_data_path,
+    float t_end=70.f,
+    float dt=0.001)
+{
+    using namespace nest::mc;
+
+    auto max_ncomp = g_trace_io.max_ncomp();
+    nlohmann::json meta = {
+        {"name", "membrane voltage"},
+        {"model", syn_type},
+        {"sim", "nestmc"},
+        {"units", "mV"},
+        {"backend", LoweredCell::backend::name()}
+    };
+
+    cell c = make_cell_ball_and_stick(false); // no stimuli
+    parameter_list syn_default(syn_type);
+    c.add_synapse({1, 0.5}, syn_default);
+    add_common_voltage_probes(c);
+
+    // injected spike events
+    postsynaptic_spike_event<float> synthetic_events[] = {
+        {{0u, 0u}, 10.0, 0.04},
+        {{0u, 0u}, 20.0, 0.04},
+        {{0u, 0u}, 40.0, 0.04}
+    };
+
+    // exclude points of discontinuity from linf analysis
+    std::vector<float> exclude = {10.f, 20.f, 40.f};
+
+    float sample_dt = 0.025f;
+    sampler_info samplers[] = {
+        {"soma.mid", {0u, 0u}, simple_sampler(sample_dt)},
+        {"dend.mid", {0u, 1u}, simple_sampler(sample_dt)},
+        {"dend.end", {0u, 2u}, simple_sampler(sample_dt)}
+    };
+
+    convergence_test_runner<int> runner("ncomp", samplers, meta);
+    runner.load_reference_data(ref_data_path);
+
+    for (int ncomp = 10; ncomp<max_ncomp; ncomp*=2) {
+        c.cable(1)->set_compartments(ncomp);
+        model<LoweredCell> m(singleton_recipe{c});
+        m.group(0).enqueue_events(synthetic_events);
+
+        runner.run(m, ncomp, t_end, dt, exclude);
+    }
+    runner.report();
+    runner.assert_all_convergence();
+}