diff --git a/tests/automake.mk b/tests/automake.mk
index 26b6d11b491a..b363928fd73d 100644
--- a/tests/automake.mk
+++ b/tests/automake.mk
@@ -20,6 +20,7 @@ COMMON_MACROS_AT = \
 TESTSUITE_AT = \
 	tests/testsuite.at \
 	tests/checkpatch.at \
+	tests/network-functions.at \
 	tests/ovn.at \
 	tests/ovn-northd.at \
 	tests/ovn-nbctl.at \
diff --git a/tests/network-functions.at b/tests/network-functions.at
new file mode 100644
index 000000000000..79aa4d899d88
--- /dev/null
+++ b/tests/network-functions.at
@@ -0,0 +1,18 @@
+AT_BANNER([test library internal helpers])
+
+OVS_START_SHELL_HELPERS
+# ip_to_hex 192 168 0 1 -> c0a80001
+# ip_to_hex 192.168.0.1 -> c0a80001
+ip_to_hex() {
+    if test $# = 1; then
+        set $(echo $1 | sed 's/\./ /g')
+    fi
+    printf "%02x%02x%02x%02x" "$@"
+}
+OVS_END_SHELL_HELPERS
+
+AT_SETUP([ip_to_hex])
+AT_KEYWORDS([network-functions])
+AT_CHECK([ip_to_hex 192 168 0 1], [0], [c0a80001])
+AT_CHECK([ip_to_hex 192.168.0.1], [0], [c0a80001])
+AT_CLEANUP
diff --git a/tests/ovn.at b/tests/ovn.at
index 35f53c06f0d1..245a6feb202b 100644
--- a/tests/ovn.at
+++ b/tests/ovn.at
@@ -1860,10 +1860,6 @@ test_arp() {
     fi
 }
 
-ip_to_hex() {
-    printf "%02x%02x%02x%02x" "$@"
-}
-
 # Send packets between all pairs of source and destination ports:
 #
 # 1. Unicast packets are delivered to exactly one logical switch port
@@ -2136,10 +2132,6 @@ test_packet() {
     done
 }
 
-ip_to_hex() {
-    printf "%02x%02x%02x%02x" "$@"
-}
-
 # Send packets between all pairs of source and destination ports:
 #
 # 1. Unicast packets are delivered to exactly one logical switch port
@@ -3260,9 +3252,6 @@ done
 # ARP request should not be responded to by logical switch router
 # type arp responder on HV1 and HV2 and should reach directly to
 # vif1 and vif2
-ip_to_hex() {
-    printf "%02x%02x%02x%02x" "$@"
-}
 sha=f00000000003
 spa=`ip_to_hex 192 168 1 2`
 tpa=`ip_to_hex 192 168 1 1`
@@ -3664,9 +3653,6 @@ as hv1 ovs-ofctl dump-flows br-int
 #
 # 2. Broadcast IP packets are delivered to all logical switch ports
 #    except the input port.
-ip_to_hex() {
-    printf "%02x%02x%02x%02x" "$@"
-}
 for is in 1 2 3; do
   for js in 1 2 3; do
     for ks in 1 2 3; do
@@ -4027,10 +4013,6 @@ test_arp() {
     fi
 }
 
-ip_to_hex() {
-    printf "%02x%02x%02x%02x" "$@"
-}
-
 # lp11 send GARP request to announce ownership of 192.168.1.100.
 
 sha=f00000000011
@@ -4233,10 +4215,6 @@ test_icmpv6() {
     done
 }
 
-ip_to_hex() {
-    printf "%02x%02x%02x%02x" "$@"
-}
-
 # no port security
 sip=`ip_to_hex 192 168 0 12`
 tip=`ip_to_hex 192 168 0 13`
@@ -4684,9 +4662,6 @@ ovs-vsctl -- add-port br-int vif2 -- \
 sleep 1
 
 # Send ip packets between the two ports.
-ip_to_hex() {
-    printf "%02x%02x%02x%02x" "$@"
-}
 
 # Packet to send.
 src_mac="f00000010203"
@@ -4801,9 +4776,6 @@ ovs-vsctl -- add-port br-int vif2 -- \
 sleep 1
 
 # Send ip packets between the two ports.
-ip_to_hex() {
-    printf "%02x%02x%02x%02x" "$@"
-}
 
 # Packet to send.
 src_mac="f00000010203"
@@ -4951,10 +4923,6 @@ OVN_POPULATE_ARP
 # XXX This should be more systematic.
 sleep 1
 
-ip_to_hex() {
-    printf "%02x%02x%02x%02x" "$@"
-}
-
 # Send ip packets between foo1 and alice1
 src_mac="f00000010203"
 dst_mac="000000010203"
@@ -5174,10 +5142,6 @@ OVN_POPULATE_ARP
 # XXX This should be more systematic.
 sleep 1
 
-ip_to_hex() {
-    printf "%02x%02x%02x%02x" "$@"
-}
-
 # Send ip packets between foo1 and alice1
 src_mac="f00000010203"
 dst_mac="000001010203"
@@ -5453,10 +5417,6 @@ options:rxq_pcap=dummy-rx.pcap
 options:rxq_pcap=${pcap_file}-rx.pcap
 }
 
-ip_to_hex() {
-    printf "%02x%02x%02x%02x" "$@"
-}
-
 AT_CAPTURE_FILE([ofctl_monitor0.log])
 as hv1 ovs-ofctl monitor br-int resume --detach --no-chdir \
 --pidfile=ovs-ofctl0.pid 2> ofctl_monitor0.log
@@ -6307,10 +6267,6 @@ ovn-nbctl lsp-add alice alice1 \
 # XXX This should be more systematic.
 sleep 2
 
-ip_to_hex() {
-    printf "%02x%02x%02x%02x" "$@"
-}
-
 # Send ip packets between foo1 and alice1
 src_mac="f00000010203"
 dst_mac="000001010203"
@@ -6448,9 +6404,6 @@ ovs-vsctl -- add-port br-int vif2 -- \
 sleep 1
 
 
-ip_to_hex() {
-    printf "%02x%02x%02x%02x" "$@"
-}
 for i in 1 2; do
     : > vif$i.expected
 done
@@ -7605,10 +7558,6 @@ ovs-vsctl -- add-port br-int hv1-vif3 -- \
 # XXX This should be more systematic.
 sleep 1
 
-ip_to_hex() {
-    printf "%02x%02x%02x%02x" "$@"
-}
-
 # Send ip packets between foo1 and foo2
 src_mac="0a0000a80103"
 dst_mac="0a0000a80104"
@@ -7841,10 +7790,6 @@ as hv1
 ovs-ofctl dump-flows
 echo "---------------------"
 
-ip_to_hex() {
-    printf "%02x%02x%02x%02x" "$@"
-}
-
 src_mac="f00000000003"
 dst_mac="f00000000001"
 src_ip=`ip_to_hex 192 168 0 2`
@@ -8928,10 +8873,6 @@ OVN_POPULATE_ARP
 # XXX This should be more systematic.
 sleep 1
 
-ip_to_hex() {
-    printf "%02x%02x%02x%02x" "$@"
-}
-
 # Test that ovn-controllers create ct-zone entry for container ports.
 foo1_zoneid=$(as hv1 ovs-vsctl get bridge br-int external_ids:ct-zone-foo1)
 AT_CHECK([test ! -z $foo1_zoneid])
@@ -9239,9 +9180,6 @@ OVN_POPULATE_ARP
 # XXX This should be more systematic.
 sleep 1
 
-ip_to_hex() {
-    printf "%02x%02x%02x%02x" "$@"
-}
 trim_zeros() {
     sed 's/\(00\)\{1,\}$//'
 }
@@ -9354,10 +9292,6 @@ echo "*************************"
 ovn-sbctl list DNS
 echo "*************************"
 
-ip_to_hex() {
-    printf "%02x%02x%02x%02x" "$@"
-}
-
 reset_pcap_file() {
     local iface=$1
     local pcap_file=$2
@@ -9845,9 +9779,6 @@ AT_CHECK([ovn-nbctl --timeout=3 --wait=sb sync], [0], [ignore])
 # Allow some time for ovn-northd and ovn-controller to catch up.
 ovn-nbctl --wait=hv sync
 
-ip_to_hex() {
-    printf "%02x%02x%02x%02x" "$@"
-}
 
 reset_pcap_file() {
     local iface=$1
@@ -10095,10 +10026,6 @@ wait_column "$hv1_ch_uuid" HA_Chassis_Group ref_chassis
 # XXX This should be more systematic.
 sleep 2
 
-ip_to_hex() {
-    printf "%02x%02x%02x%02x" "$@"
-}
-
 reset_pcap_file() {
     local iface=$1
     local pcap_file=$2
@@ -10343,9 +10270,6 @@ AT_CHECK([as hv2 ovs-ofctl dump-flows br-int | grep arp | grep load:0x2- | grep
 ])
 
 
-ip_to_hex() {
-    printf "%02x%02x%02x%02x" "$@"
-}
 
 
 : > hv2-vif1.expected
@@ -10837,10 +10761,6 @@ for chassis in hv1 hv2 hv3; do
     echo "--------------------------"
 done
 
-ip_to_hex() {
-    printf "%02x%02x%02x%02x" "$@"
-}
-
 foo1_ip=$(ip_to_hex 192 168 1 2)
 gw_ip=$(ip_to_hex 172 16 1 6)
 dst_ip=$(ip_to_hex 8 8 8 8)
@@ -11289,10 +11209,6 @@ OVN_POPULATE_ARP
 # XXX This should be more systematic.
 sleep 1
 
-ip_to_hex() {
-    printf "%02x%02x%02x%02x" "$@"
-}
-
 # Send ip packets between foo1 and alice1
 src_mac="f00000010203"
 dst_mac="000000010203"
@@ -12860,10 +12776,6 @@ OVN_POPULATE_ARP
 # allow some time for ovn-northd and ovn-controller to catch up.
 sleep 1
 
-ip_to_hex() {
-    printf "%02x%02x%02x%02x" "$@"
-}
-
 for i in 1 2 3; do
     : > vif${i}1.expected
 done
@@ -13067,9 +12979,6 @@ as hv1 ovs-ofctl dump-flows br-int
 
 # Send IP packets between all pairs of source and destination ports,
 # packets matches ACL (pg2 to pg1) should be dropped
-ip_to_hex() {
-    printf "%02x%02x%02x%02x" "$@"
-}
 for is in 1 2 3; do
   for js in 1 2 3; do
     for ks in 1 2 3; do
@@ -13307,9 +13216,6 @@ as hv1 ovs-ofctl dump-flows br-int
 
 # Send IP packets between all pairs of source and destination ports,
 # packets matches ACL1 but not ACL2 should be dropped
-ip_to_hex() {
-    printf "%02x%02x%02x%02x" "$@"
-}
 for is in 1 2 3; do
   for js in 1 2 3; do
     for ks in 1 2 3; do
@@ -13530,10 +13436,6 @@ ${dst_ip}0035111100080000
     done
 }
 
-ip_to_hex() {
-    printf "%02x%02x%02x%02x" "$@"
-}
-
 reset_pcap_file() {
     local iface=$1
     local pcap_file=$2
@@ -13732,10 +13634,6 @@ ${dst_ip}0035111100080000
     done
 }
 
-ip_to_hex() {
-    printf "%02x%02x%02x%02x" "$@"
-}
-
 reset_pcap_file() {
     local iface=$1
     local pcap_file=$2
@@ -14155,10 +14053,6 @@ test_ip6_packet() {
     as hv$hv ovs-appctl netdev-dummy/receive vif$inport $packet
 }
 
-ip_to_hex() {
-    printf "%02x%02x%02x%02x" "$@"
-}
-
 for i in 1 2; do
     net_add n$i
     ovn-nbctl ls-add sw$i
@@ -14300,10 +14194,6 @@ test_ip6_packet() {
     as hv$hv ovs-appctl netdev-dummy/receive vif$inport $packet
 }
 
-ip_to_hex() {
-    printf "%02x%02x%02x%02x" "$@"
-}
-
 for i in 1 2; do
     net_add n$i
     ovn-nbctl ls-add sw$i
@@ -14784,10 +14674,6 @@ options:rxq_pcap=dummy-rx.pcap
 options:rxq_pcap=${pcap_file}-rx.pcap
 }
 
-ip_to_hex() {
-    printf "%02x%02x%02x%02x" "$@"
-}
-
 AT_CAPTURE_FILE([ofctl_monitor0_hv1.log])
 as hv1 ovs-ofctl monitor br-int resume --detach --no-chdir \
 --pidfile=ovs-ofctl0.pid 2> ofctl_monitor0_hv1.log
@@ -15469,10 +15355,6 @@ reset_pcap_file() {
  options:rxq_pcap=${pcap_file}-rx.pcap
 }
 
-ip_to_hex() {
-     printf "%02x%02x%02x%02x" "$@"
-}
-
 test_ip_packet_larger() {
     local icmp_pmtu_reply_expected=$1
 
@@ -15750,9 +15632,6 @@ ovn-nbctl lr-nat-add lr0 snat 2002::1 2001::/64
 OVN_POPULATE_ARP
 ovn-nbctl --wait=hv sync
 
-ip_to_hex() {
-    printf "%02x%02x%02x%02x" "$@"
-}
 
 src_mac=f00000010203
 src_ip=$(ip_to_hex 192 168 1 2)
@@ -16210,10 +16089,6 @@ lrp_to_lrp_mac () {
     esac
 }
 
-ip_to_hex() {
-       printf "%02x%02x%02x%02x" "$@"
-}
-
 net_add n1
 for i in 1 2; do
     sim_add hv$i
@@ -16445,10 +16320,6 @@ AT_CHECK([cat lflows.txt], [0], [dnl
   table=13(lr_in_arp_resolve  ), priority=100  , match=(outport == "lr0-sw0" && reg0 == 10.0.0.10), action=(eth.dst = 00:00:00:00:00:00; next;)
 ])
 
-ip_to_hex() {
-    printf "%02x%02x%02x%02x" "$@"
-}
-
 hv1_ch_uuid=`ovn-sbctl --bare --columns _uuid find chassis name="hv1"`
 hv2_ch_uuid=`ovn-sbctl --bare --columns _uuid find chassis name="hv2"`
 
@@ -16847,10 +16718,6 @@ options:rxq_pcap=dummy-rx.pcap
 options:rxq_pcap=${pcap_file}-rx.pcap
 }
 
-ip_to_hex() {
-     printf "%02x%02x%02x%02x" "$@"
-}
-
 #
 # send_igmp_v3_report INPORT HV ETH_SRC IP_SRC IP_CSUM GROUP REC_TYPE
 #                     IGMP_CSUM OUTFILE
@@ -18438,10 +18305,6 @@ m4_define([DVR_N_S_PING],
        esac
    }
 
-   ip_to_hex() {
-          printf "%02x%02x%02x%02x" "$@"
-   }
-
    net_add n1
    for i in 1 2; do
        sim_add hv$i
@@ -18579,11 +18442,6 @@ m4_define([DVR_N_S_PING],
        echo router-to-`vif_to_ls ${1}`
    }
 
-   ip_to_hex() {
-          printf "%02x%02x%02x%02x" "${@}"
-   }
-
-
    test_ip() {
            # This packet has bad checksums but logical L3 routing doesn't check.
            local inport=${1} src_mac=${2} dst_mac=${3} src_ip=${4} dst_ip=${5} outport=${6}
@@ -18761,10 +18619,6 @@ ovn-nbctl --wait=hv sync
 
 as hv1 ovs-appctl -t ovn-controller vlog/set dbg
 
-ip_to_hex() {
-    printf "%02x%02x%02x%02x" "$@"
-}
-
 # From sw0-p1 send GARP for 10.0.0.30.
 # ovn-controller should learn the
 #   mac_binding entry
@@ -18922,10 +18776,6 @@ ovn-nbctl lsp-set-addresses ln1 unknown
 ovn-nbctl lsp-set-type ln1 localnet
 ovn-nbctl lsp-set-options ln1 network_name=phys
 
-ip_to_hex() {
-       printf "%02x%02x%02x%02x" "$@"
-}
-
 net_add n1
 
 sim_add hv1
@@ -19035,10 +18885,6 @@ vif_to_hv () {
     esac
 }
 
-ip_to_hex() {
-       printf "%02x%02x%02x%02x" "$@"
-}
-
 net_add n1
 for i in 1 2; do
     sim_add hv$i
@@ -19177,11 +19023,6 @@ vif_to_lrp () {
     echo router-to-`vif_to_ls $1`
 }
 
-ip_to_hex() {
-       printf "%02x%02x%02x%02x" "$@"
-}
-
-
 test_ip() {
         # This packet has bad checksums but logical L3 routing doesn't check.
         local inport=$1 src_mac=$2 dst_mac=$3 src_ip=$4 dst_ip=$5 outport=$6
@@ -19594,10 +19435,6 @@ AT_CLEANUP
 AT_SETUP([ovn -- ARP/ND request broadcast limiting])
 ovn_start
 
-ip_to_hex() {
-    printf "%02x%02x%02x%02x" "$@"
-}
-
 send_arp_request() {
     local hv=$1 inport=$2 eth_src=$3 spa=$4 tpa=$5
     local eth_dst=ffffffffffff
@@ -20465,10 +20302,6 @@ options:rxq_pcap=dummy-rx.pcap
 options:rxq_pcap=${pcap_file}-rx.pcap
 }
 
-ip_to_hex() {
-     printf "%02x%02x%02x%02x" "$@"
-}
-
 build_udp() {
     local sport=$1 dport=$2 chksum=$3
     local len=000a
@@ -20816,10 +20649,6 @@ table=0, priority=100, pkt_mark=0xffffffff actions=drop
 ])
 
 AT_CHECK([as hv1 ovs-ofctl --protocols=OpenFlow13 add-flows br-phys flows.txt])
-
-ip_to_hex() {
-     printf "%02x%02x%02x%02x" "$@"
-}
 sleep 5
 
 send_ipv4_pkt() {
@@ -21356,10 +21185,6 @@ test_arp() {
     fi
 }
 
-ip_to_hex() {
-    printf "%02x%02x%02x%02x" "$@"
-}
-
 sw0p1_ip=$(ip_to_hex 192 168 1 2)
 rtr_ip=$(ip_to_hex 192 168 1 1)
 test_arp 1 1 000000010102 $sw0p1_ip $rtr_ip 000000000001
@@ -21679,10 +21504,6 @@ ovn-nbctl --wait=hv sync
 AT_CHECK([ovn-sbctl lflow-list | grep lr_in_arp_resolve | grep 10.0.0.1], [1], [])
 AT_CHECK([ovn-sbctl lflow-list | grep lr_in_arp_resolve | grep 10.0.0.2], [1], [])
 
-ip_to_hex() {
-    printf "%02x%02x%02x%02x" "$@"
-}
-
 # Send ip packets from p1 to p2
 src_mac="f00000000102"
 dst_mac="000000000101"
@@ -21921,10 +21742,6 @@ sw_key=$(ovn-sbctl --bare --columns tunnel_key list datapath_binding r1)
 
 AT_CHECK([ovn-sbctl lflow-list | grep lr_in_arp_resolve | grep 10.0.1.1], [1], [])
 
-ip_to_hex() {
-    printf "%02x%02x%02x%02x" "$@"
-}
-
 # Send ip packets from p1 to lrp-r1-s1
 src_mac="f00000000102"
 dst_mac="000000000101"
@@ -22227,10 +22044,6 @@ options:rxq_pcap=dummy-rx.pcap
 options:rxq_pcap=${pcap_file}-rx.pcap
 }
 
-ip_to_hex() {
-    printf "%02x%02x%02x%02x" "$@"
-}
-
 send_arp_request() {
     local eth_src=$1 spa=$2 tpa=$3
     local eth_dst=ffffffffffff
@@ -22384,10 +22197,6 @@ ${dst_ip}0035111100080000
     done
 }
 
-ip_to_hex() {
-    printf "%02x%02x%02x%02x" "$@"
-}
-
 reset_pcap_file() {
     local iface=$1
     local pcap_file=$2
@@ -22504,10 +22313,6 @@ ${dst_ip}0035111100080000
     done
 }
 
-ip_to_hex() {
-    printf "%02x%02x%02x%02x" "$@"
-}
-
 reset_pcap_file() {
     local iface=$1
     local pcap_file=$2
diff --git a/tests/testsuite.at b/tests/testsuite.at
index 1985923d5bc5..960227dcc75f 100644
--- a/tests/testsuite.at
+++ b/tests/testsuite.at
@@ -20,6 +20,7 @@ m4_include([tests/ovs-macros.at])
 m4_include([tests/ovsdb-macros.at])
 m4_include([tests/ofproto-macros.at])
 m4_include([tests/ovn-macros.at])
+m4_include([tests/network-functions.at])
 
 m4_include([tests/ovn.at])
 m4_include([tests/ovn-performance.at])
