diff --git a/tests/system-dpdk-macros.at b/tests/system-dpdk-macros.at
index f29b3c283f..ce8e98f475 100644
--- a/tests/system-dpdk-macros.at
+++ b/tests/system-dpdk-macros.at
@@ -124,3 +124,98 @@ m4_define([OVS_DPDK_STOP_TESTPMD],
   [AT_CHECK([kill `cat testpmd.pid`])
    OVS_WAIT([kill -0 `cat testpmd.pid`], [kill -9 `cat testpmd.pid`])
 ])
+
+
+# OVS_TRAFFIC_VSWITCHD_START([vsctl-args], [vsctl-output], [dbinit-aux-args])
+#
+# Creates a database and starts ovsdb-server, starts ovs-vswitchd
+# connected to that database, calls ovs-vsctl to create a bridge named
+# br0 with predictable settings, passing 'vsctl-args' as additional
+# commands to ovs-vsctl.  If 'vsctl-args' causes ovs-vsctl to provide
+# output (e.g. because it includes "create" commands) then 'vsctl-output'
+# specifies the expected output after filtering through uuidfilt.
+# 'dbinit-aux-args' are passed as additional commands to 'ovs-vsctl init'
+# before starting ovs-vswitchd.
+m4_define([OVS_TRAFFIC_VSWITCHD_START],
+  [
+   OVS_DPDK_PRE_CHECK()
+   OVS_WAIT_WHILE([ip link show ovs-netdev])
+   dnl For functional tests, no need for DPDK PCI probing.
+   OVS_DPDK_START([--no-pci], [--disable-system], [$3])
+   dnl Add bridges, ports, etc.
+   OVS_WAIT_WHILE([ip link show br0])
+   AT_CHECK([ovs-vsctl -- _ADD_BR([br0]) -- $1 m4_if([$2], [], [], [| uuidfilt])], [0], [$2])
+])
+
+
+# OVS_TRAFFIC_VSWITCHD_STOP([ALLOWLIST], [extra_cmds])
+#
+# Gracefully stops ovs-vswitchd and ovsdb-server, checking their log files
+# for messages with severity WARN or higher and signaling an error if any
+# is present.  The optional ALLOWLIST may contain shell-quoted "sed"
+# commands to delete any warnings that are actually expected, e.g.:
+#
+#   OVS_TRAFFIC_VSWITCHD_STOP(["/expected error/d"])
+#
+# 'extra_cmds' are shell commands to be executed after OVS_VSWITCHD_STOP() is
+# invoked. They can be used to perform additional cleanups such as name space
+# removal.
+m4_define([OVS_TRAFFIC_VSWITCHD_STOP],
+  [OVS_DPDK_STOP_VSWITCHD([$1";"ADD_VETH_IGNORE_LOGS";dnl
+/does not exist. The Open vSwitch kernel module is probably not loaded./d"])
+   AT_CHECK([:; $2])
+])
+
+
+m4_define([OVS_CHECK_8021AD],
+    [AT_SKIP_IF([:])])
+
+
+m4_define([OVS_CHECK_TC_QDISC],
+    [AT_SKIP_IF([:])])
+
+
+m4_define([OVS_CHECK_TCPDUMP],
+    [AT_SKIP_IF([:])])
+
+
+# Fake a veth by creating a tap on kernel side and plug it in OVS using the
+# net/tap DPDK driver.
+m4_define([ADD_VETH],
+    dnl By default, OVS starts as many PMD threads as NUMA sockets.
+    dnl One txq is required for each PMD thread. One additional txq is required
+    dnl for OVS main thread. On the other hand, a tap device expects
+    dnl n_rxq == n_txq.
+    [ n_rxq=$(lscpu | awk '/^NUMA node\(s\):/ { print $NF + 1; }')
+      AT_CHECK([ovs-vsctl add-port $3 ovs-$1 -- \
+                set interface ovs-$1 external-ids:iface-id="$1" -- \
+                set interface ovs-$1 type=dpdk -- \
+                set interface ovs-$1 options:n_rxq=${n_rxq} -- \
+                set interface ovs-$1 options:dpdk-devargs=net_tap$1,iface=$1])
+      OVS_WAIT_UNTIL([ip link show dev $1 | grep -qw LOWER_UP])
+      AT_CHECK([ip link set $1 netns $2])
+      NS_CHECK_EXEC([$2], [ip addr add $4 dev $1 $7])
+      NS_CHECK_EXEC([$2], [ip link set dev $1 up])
+      if test -n "$5"; then
+        NS_CHECK_EXEC([$2], [ip link set dev $1 address $5])
+      fi
+      if test -n "$6"; then
+        NS_CHECK_EXEC([$2], [ip route add default via $6])
+      fi
+    ]
+)
+
+
+# Using a net/tap port with ADD_VETH can trigger some warning logs that can be
+# filtered with the following macro:
+m4_define([ADD_VETH_IGNORE_LOGS],
+    ["dnl
+/tap_nl_dump_ext_ack(): Specified qdisc kind is unknown/d
+/qdisc_create_multiq(): Could not add multiq qdisc (2): No such file or directory/d
+/eth_dev_tap_create(): .*: failed to create multiq qdisc./d
+/eth_dev_tap_create():  Disabling rte flow support: No such file or directory/d
+/tap_mp_req_on_rxtx(): Failed to send start req to secondary/d"])
+
+
+m4_define([CONFIGURE_VETH_OFFLOADS],
+   [AT_CHECK([ethtool -K $1 tx off], [0], [ignore], [ignore])])
diff --git a/tests/system-dpdk-testsuite.at b/tests/system-dpdk-testsuite.at
index 382f09e9ff..f61fbf9212 100644
--- a/tests/system-dpdk-testsuite.at
+++ b/tests/system-dpdk-testsuite.at
@@ -20,6 +20,8 @@ m4_include([tests/ovs-macros.at])
 m4_include([tests/ovsdb-macros.at])
 m4_include([tests/ofproto-macros.at])
 m4_include([tests/system-common-macros.at])
+m4_include([tests/system-userspace-macros.at])
 m4_include([tests/system-dpdk-macros.at])
 
 m4_include([tests/system-dpdk.at])
+m4_include([tests/system-traffic.at])
diff --git a/tests/system-dpdk.at b/tests/system-dpdk.at
index e8a04d1d86..3c812fa6e3 100644
--- a/tests/system-dpdk.at
+++ b/tests/system-dpdk.at
@@ -1,6 +1,3 @@
-m4_define([CONFIGURE_VETH_OFFLOADS],
-   [AT_CHECK([ethtool -K $1 tx off], [0], [ignore], [ignore])])
-
 AT_BANNER([OVS-DPDK unit tests])
 
 dnl CHECK_MEMPOOL_PARAM([mtu], [numa], [+line])
@@ -140,7 +137,7 @@ OVS_WAIT_UNTIL([grep "vHost Device '$OVS_RUNDIR/dpdkvhostuser0' has been removed
 
 dnl Clean up
 AT_CHECK([ovs-vsctl del-port br10 dpdkvhostuser0], [], [stdout], [stderr])
-OVS_DPDK_STOP_VSWITCHD(["dnl
+OVS_DPDK_STOP_VSWITCHD([ADD_VETH_IGNORE_LOGS";dnl
 /VHOST_CONFIG: (.*dpdkvhostuser0) recvmsg failed/d
 /VHOST_CONFIG: (.*dpdkvhostuser0) failed to connect: No such file or directory/d
 /dpdkvhostuser ports are considered deprecated;  please migrate to dpdkvhostuserclient ports./d
@@ -226,7 +223,7 @@ OVS_DPDK_STOP_TESTPMD()
 
 dnl Clean up
 AT_CHECK([ovs-vsctl del-port br10 dpdkvhostuserclient0], [], [stdout], [stderr])
-OVS_DPDK_STOP_VSWITCHD(["dnl
+OVS_DPDK_STOP_VSWITCHD([ADD_VETH_IGNORE_LOGS";dnl
 /VHOST_CONFIG: (.*dpdkvhostclient0) recvmsg failed/d
 /VHOST_CONFIG: (.*dpdkvhostclient0) failed to connect: No such file or directory/d
 /dpdkvhostuser ports are considered deprecated;  please migrate to dpdkvhostuserclient ports./d
