diff --git a/northd/ovn-northd.c b/northd/ovn-northd.c
index d989ddf53..10671edb7 100644
--- a/northd/ovn-northd.c
+++ b/northd/ovn-northd.c
@@ -35,6 +35,7 @@
 #include "lib/ovn-nb-idl.h"
 #include "lib/ovn-sb-idl.h"
 #include "lib/ovn-util.h"
+#include "lib/unit-test.h"
 #include "ovn/actions.h"
 #include "ovn/logical-fields.h"
 #include "packets.h"
@@ -13050,6 +13051,8 @@ main(int argc, char *argv[])
                              cluster_state_reset_cmd,
                              &reset_ovnnb_idl_min_index);
 
+    register_unixctl_unit_test();
+
     daemonize_complete();
 
     /* We want to detect (almost) all changes to the ovn-nb db. */
@@ -13464,3 +13467,176 @@ cluster_state_reset_cmd(struct unixctl_conn *conn, int argc OVS_UNUSED,
     poll_immediate_wake();
     unixctl_command_reply(conn, NULL);
 }
+
+#ifdef ENABLE_UNIT_TESTS
+
+static enum test_result
+test_init_ipam_ipv6_prefix(void *ignore OVS_UNUSED)
+{
+    struct ipam_info ipam;
+    struct {
+        const char *prefix;
+        bool expected_ipv6_prefix_set;
+        struct in6_addr expected_ipv6_prefix;
+    } test_cases[] = {
+        /* No prefix */
+        { NULL,         false, in6addr_any },
+        /* Bad prefix */
+        { "aef02::",    false, in6addr_any},
+        /* Bad subnet size */
+        { "aef0::/8",   false, in6addr_any},
+        /* Good prefix, no subnet size */
+        { "aef0::",     true,  {{{0xae, 0xf0}}}},
+        /* Good prefix, good subnet size */
+        { "aef0::/64",  true,  {{{0xae, 0xf0}}}},
+    };
+
+    enum test_result result = OVN_TEST_PASS;
+
+    for (size_t i = 0; i < ARRAY_SIZE(test_cases); i++) {
+        memset(&ipam, 0, sizeof ipam);
+        init_ipam_ipv6_prefix(test_cases[i].prefix, &ipam);
+        if (ipam.ipv6_prefix_set != test_cases[i].expected_ipv6_prefix_set) {
+            VLOG_WARN("Expected ipv6_prefix_set to be %s but got %s",
+                      test_cases[i].expected_ipv6_prefix_set ? "true" : "false",
+                      ipam.ipv6_prefix_set ? "true" : "false");
+            result = OVN_TEST_FAIL;
+            continue;
+        }
+        if (ipam.ipv6_prefix_set
+            && !IN6_ARE_ADDR_EQUAL(&ipam.ipv6_prefix,
+                                   &test_cases[i].expected_ipv6_prefix)) {
+            char expected[INET6_ADDRSTRLEN];
+            char actual[INET6_ADDRSTRLEN];
+            inet_ntop(AF_INET6, &ipam.ipv6_prefix,
+                      actual, sizeof actual);
+            inet_ntop(AF_INET6, &test_cases[i].expected_ipv6_prefix,
+                      expected, sizeof expected);
+            VLOG_WARN("Expected IPv6 prefix %s but got %s",
+                      expected, actual);
+            result = OVN_TEST_FAIL;
+            continue;
+        }
+    }
+
+    return result;
+}
+
+UNIT_TEST_DEFINE(init_ipam_ipv6_prefix, test_init_ipam_ipv6_prefix, NULL);
+
+static enum test_result
+test_init_ipam_ipv4(void *ignore OVS_UNUSED)
+{
+    struct {
+        const char *subnet;
+        const char *exclude;
+        struct ipam_info info;
+        uint32_t expected_start_ip;
+        size_t expected_total_ips;
+        unsigned long *expected_allocated_ips;
+    } test_cases[] = {
+        { "192.168.0.0/24", NULL, {0,}, 0xc0a80001, 255,
+          bitmap_set1(bitmap_allocate(255), 0) },
+        { "192.168.0.0/24", "192.168.0.10", {0,}, 0xc0a80001, 255,
+          bitmap_set1(bitmap_set1(bitmap_allocate(255), 0), 9)},
+        { "192.168.0.0/24", "192.168.0.10..192.168.0.19", {0, }, 0xc0a80001, 255,
+          bitmap_set_multiple(bitmap_set1(bitmap_allocate(255), 0), 9, 10, true)},
+    };
+    enum test_result result = OVN_TEST_PASS;
+
+    for (size_t i = 0; i < ARRAY_SIZE(test_cases); i++) {
+        init_ipam_ipv4(test_cases[i].subnet,
+                       test_cases[i].exclude,
+                       &test_cases[i].info);
+
+        if (test_cases[i].info.start_ipv4 != test_cases[i].expected_start_ip) {
+            VLOG_WARN("Mismatched starting IP. Expected %" PRIu32
+                      " but got %" PRIu32 "\n",
+                      test_cases[i].expected_start_ip,
+                      test_cases[i].info.start_ipv4);
+            result = OVN_TEST_FAIL;
+            continue;
+        }
+        if (test_cases[i].info.total_ipv4s != test_cases[i].expected_total_ips) {
+            VLOG_WARN("Mismatched total IPs. Expected %" PRIuSIZE
+                      " but got %" PRIuSIZE "\n",
+                      test_cases[i].expected_total_ips,
+                      test_cases[i].info.total_ipv4s);
+            result = OVN_TEST_FAIL;
+            continue;
+        }
+        if (!bitmap_equal(test_cases[i].info.allocated_ipv4s,
+                          test_cases[i].expected_allocated_ips,
+                          test_cases[i].expected_total_ips)) {
+            VLOG_WARN("Mismatched allocated IPs\n");
+            result = OVN_TEST_FAIL;
+            continue;
+        }
+    }
+
+    for (size_t i = 0; i < ARRAY_SIZE(test_cases); i++) {
+        bitmap_free(test_cases[i].info.allocated_ipv4s);
+        bitmap_free(test_cases[i].expected_allocated_ips);
+    }
+
+    return result;
+}
+
+UNIT_TEST_DEFINE(init_ipam_ipv4, test_init_ipam_ipv4, NULL);
+
+static enum test_result
+test_ipam_get_unused_ip(void *ignore OVS_UNUSED)
+{
+    enum test_result result = OVN_TEST_PASS;
+    struct ipam_info info;
+
+    struct smap config = SMAP_INITIALIZER(&config);
+    smap_add(&config, "subnet", "192.168.0.0/29");
+    smap_add(&config, "exclude_ips", "192.168.0.5");
+    init_ipam_info(&info, &config);
+
+    uint32_t expected_ip [] = {
+        /* First IP we retrieve will be 192.168.0.2 since
+         * 192.168.0.1 is reserved for the connected router
+         */
+        0xc0a80002,
+        0xc0a80003,
+        0xc0a80004,
+        /* We should skip 192.168.0.5 since it is excluded */
+        0xc0a80006,
+        /* After 192.168.0.6, we should be out of space for the
+         * subnet.
+         */
+        0,
+        /* Just to be safe, let's make sure we get the same result
+         * when we try again.
+         */
+        0,
+    };
+
+    for (size_t i = 0; i < ARRAY_SIZE(expected_ip); i++) {
+        uint32_t next_ip;
+
+        next_ip = ipam_get_unused_ip(&info);
+        if (next_ip != expected_ip[i]) {
+            VLOG_WARN("Expected IP address "IP_FMT" but got "IP_FMT" instead",
+                      IP_ARGS(htonl(expected_ip[i])), IP_ARGS(htonl(next_ip)));
+            result = OVN_TEST_FAIL;
+            break;
+        }
+        if (next_ip && !ipam_insert_ip(&info, next_ip)) {
+            VLOG_WARN("Unable to insert ip "IP_FMT" into IPAM",
+                      IP_ARGS(htonl(next_ip)));
+            result = OVN_TEST_FAIL;
+            break;
+        }
+    }
+
+    smap_destroy(&config);
+    bitmap_free(info.allocated_ipv4s);
+    return result;
+}
+
+UNIT_TEST_DEFINE(ipam_get_unused_ip, test_ipam_get_unused_ip, NULL);
+
+#endif /* ENABLE_UNIT_TESTS */
diff --git a/tests/automake.mk b/tests/automake.mk
index 26b6d11b4..9ad577576 100644
--- a/tests/automake.mk
+++ b/tests/automake.mk
@@ -30,7 +30,8 @@ TESTSUITE_AT = \
 	tests/ovn-controller-vtep.at \
 	tests/ovn-ic.at \
 	tests/ovn-macros.at \
-	tests/ovn-performance.at
+	tests/ovn-performance.at \
+	tests/ovn-unit-tests.at
 
 SYSTEM_KMOD_TESTSUITE_AT = \
 	tests/system-common-macros.at \
diff --git a/tests/ovn-unit-tests.at b/tests/ovn-unit-tests.at
new file mode 100644
index 000000000..b7d16667c
--- /dev/null
+++ b/tests/ovn-unit-tests.at
@@ -0,0 +1,26 @@
+AT_BANNER([OVN unit tests])
+
+AT_SETUP([ovn -- unit test -- init_ipam_ipv4])
+AT_SKIP_IF([test "$ENABLE_UNIT_TESTS" = no])
+ovn_start
+AT_CHECK([ovn-appctl -t northd/ovn-northd unit-test init_ipam_ipv4], [0], [dnl
+Unit test init_ipam_ipv4 passed
+])
+AT_CLEANUP
+
+AT_SETUP([ovn -- unit test -- init_ipam_ipv6_prefix])
+AT_SKIP_IF([test "$ENABLE_UNIT_TESTS" = no])
+ovn_start
+AT_CHECK([ovn-appctl -t northd/ovn-northd unit-test init_ipam_ipv6_prefix], [0], [dnl
+Unit test init_ipam_ipv6_prefix passed
+])
+AT_CLEANUP
+
+AT_SETUP([ovn -- unit test -- ipam_get_unused_ip])
+AT_SKIP_IF([test "$ENABLE_UNIT_TESTS" = no])
+ovn_start
+AT_CHECK([ovn-appctl -t northd/ovn-northd unit-test ipam_get_unused_ip], [0], [dnl
+Unit test ipam_get_unused_ip passed
+])
+AT_CLEANUP
+
diff --git a/tests/testsuite.at b/tests/testsuite.at
index 1985923d5..5e60bad73 100644
--- a/tests/testsuite.at
+++ b/tests/testsuite.at
@@ -21,6 +21,7 @@ m4_include([tests/ovsdb-macros.at])
 m4_include([tests/ofproto-macros.at])
 m4_include([tests/ovn-macros.at])
 
+m4_include([tests/ovn-unit-tests.at])
 m4_include([tests/ovn.at])
 m4_include([tests/ovn-performance.at])
 m4_include([tests/ovn-northd.at])
