Repository: cilium/ebpf
Pull request #1702 (2025-02-24 21:22:11)

ebpf: clean up tests
Introduce helpers for creating a map, program, collection from a spec. Additionally, add helpers which create very common types like arrays or socket filter programs.

This significantly cuts down on boiler plate in unit tests, and ensures that all resources backed by a file descriptor are cleaned up.

It will also allow adding compatibility code for running Linux unit tests on Windows in a central location.


Patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index b746269..29c2a99 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -9,7 +9,7 @@ env:
   TMPDIR: /tmp
   CI_MAX_KERNEL_VERSION: '6.11'
   CI_MAX_EFW_VERSION: '0.20.0'
-  CI_MIN_CLANG_VERSION: '11'
+  CI_MIN_CLANG_VERSION: '13'
   go_version: '~1.24'
   prev_go_version: '~1.23'
   CGO_ENABLED: '0'
@@ -48,7 +48,6 @@ jobs:
 
       - name: Test bpf2go
         run: |
-          sudo apt-get install clang-11 llvm-11
           go test -v ./cmd/bpf2go
 
       - name: Build examples
diff --git a/go.mod b/go.mod
index 24891f9..3d27187 100644
--- a/go.mod
+++ b/go.mod
@@ -3,7 +3,7 @@ module github.com/cilium/ebpf
 go 1.23
 
 require (
-	github.com/go-quicktest/qt v1.101.0
+	github.com/go-quicktest/qt v1.101.1-0.20240301121107-c6c8733fa1e6
 	github.com/google/go-cmp v0.6.0
 	github.com/jsimonetti/rtnetlink/v2 v2.0.1
 	golang.org/x/sys v0.30.0
@@ -15,7 +15,7 @@ require (
 	github.com/kr/text v0.2.0 // indirect
 	github.com/mdlayher/netlink v1.7.2 // indirect
 	github.com/mdlayher/socket v0.4.1 // indirect
-	github.com/rogpeppe/go-internal v1.11.0 // indirect
+	github.com/rogpeppe/go-internal v1.12.0 // indirect
 	golang.org/x/net v0.33.0 // indirect
 	golang.org/x/sync v0.1.0 // indirect
 )
diff --git a/go.sum b/go.sum
index c154a61..d9a9f1c 100644
--- a/go.sum
+++ b/go.sum
@@ -1,6 +1,8 @@
 github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
 github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI=
 github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow=
+github.com/go-quicktest/qt v1.101.1-0.20240301121107-c6c8733fa1e6 h1:teYtXy9B7y5lHTp8V9KPxpYRAVA7dozigQcMiBust1s=
+github.com/go-quicktest/qt v1.101.1-0.20240301121107-c6c8733fa1e6/go.mod h1:p4lGIVX+8Wa6ZPNDvqcxq36XpUDLh42FLetFU7odllI=
 github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
 github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
@@ -19,6 +21,8 @@ github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsK
 github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
 github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
 github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
+github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
+github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
 golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
 golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
 golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
diff --git a/map.go b/map.go
index e206b9e..318481c 100644
--- a/map.go
+++ b/map.go
@@ -293,7 +293,7 @@ func newMapFromFD(fd *sys.FD) (*Map, error) {
 		return nil, fmt.Errorf("get map info: %w", err)
 	}
 
-	return newMap(fd, info.Name, info.Type, info.KeySize, info.ValueSize, info.MaxEntries, info.Flags)
+	return newMapFromParts(fd, info.Name, info.Type, info.KeySize, info.ValueSize, info.MaxEntries, info.Flags)
 }
 
 // NewMap creates a new Map.
@@ -520,7 +520,7 @@ func (spec *MapSpec) createMap(inner *sys.FD) (_ *Map, err error) {
 	}
 
 	defer closeOnError(fd)
-	m, err := newMap(fd, spec.Name, spec.Type, spec.KeySize, spec.ValueSize, spec.MaxEntries, spec.Flags)
+	m, err := newMapFromParts(fd, spec.Name, spec.Type, spec.KeySize, spec.ValueSize, spec.MaxEntries, spec.Flags)
 	if err != nil {
 		return nil, fmt.Errorf("map create: %w", err)
 	}
@@ -580,9 +580,9 @@ func handleMapCreateError(attr sys.MapCreateAttr, spec *MapSpec, err error) erro
 	return fmt.Errorf("map create: %w", err)
 }
 
-// newMap allocates and returns a new Map structure.
+// newMapFromParts allocates and returns a new Map structure.
 // Sets the fullValueSize on per-CPU maps.
-func newMap(fd *sys.FD, name string, typ MapType, keySize, valueSize, maxEntries, flags uint32) (*Map, error) {
+func newMapFromParts(fd *sys.FD, name string, typ MapType, keySize, valueSize, maxEntries, flags uint32) (*Map, error) {
 	m := &Map{
 		name,
 		fd,


Test patch:
diff --git a/collection_test.go b/collection_test.go
index 4d484cf..93d78fc 100644
--- a/collection_test.go
+++ b/collection_test.go
@@ -10,7 +10,6 @@ import (
 	"testing"
 
 	"github.com/go-quicktest/qt"
-	"github.com/google/go-cmp/cmp"
 
 	"github.com/cilium/ebpf/asm"
 	"github.com/cilium/ebpf/btf"
@@ -46,11 +45,7 @@ func TestCollectionSpecNotModified(t *testing.T) {
 		},
 	}
 
-	coll, err := NewCollection(&cs)
-	if err != nil {
-		t.Fatal(err)
-	}
-	coll.Close()
+	mustNewCollection(t, &cs, nil)
 
 	if cs.Programs["test"].Instructions[0].Constant != 0 {
 		t.Error("Creating a collection modifies input spec")
@@ -107,17 +102,11 @@ func TestCollectionSpecLoadCopy(t *testing.T) {
 		Prog *Program `ebpf:"xdp_prog"`
 	}
 
-	err = spec.LoadAndAssign(&objs, nil)
-	testutils.SkipIfNotSupported(t, err)
-	if err != nil {
-		t.Fatal("Loading original spec:", err)
-	}
-	defer objs.Prog.Close()
+	mustLoadAndAssign(t, spec, &objs, nil)
+	qt.Assert(t, qt.IsNil(objs.Prog.Close()))
 
-	if err := spec2.LoadAndAssign(&objs, nil); err != nil {
-		t.Fatal("Loading copied spec:", err)
-	}
-	defer objs.Prog.Close()
+	mustLoadAndAssign(t, spec2, &objs, nil)
+	qt.Assert(t, qt.IsNil(objs.Prog.Close()))
 }
 
 func TestCollectionSpecLoadMutate(t *testing.T) {
@@ -133,14 +122,9 @@ func TestCollectionSpecLoadMutate(t *testing.T) {
 		Prog *Program `ebpf:"xdp_prog"`
 	}
 
-	err = spec.LoadAndAssign(&objs, nil)
-	testutils.SkipIfNotSupported(t, err)
-	qt.Assert(t, qt.IsNil(err))
-	defer objs.Prog.Close()
-
-	if diff := cmp.Diff(orig, spec, csCmpOpts...); diff != "" {
-		t.Errorf("CollectionSpec was mutated after loading (-want +got):\n%s", diff)
-	}
+	mustLoadAndAssign(t, spec, &objs, nil)
+	qt.Assert(t, qt.IsNil(objs.Prog.Close()))
+	qt.Assert(t, qt.CmpEquals(spec, orig, csCmpOpts))
 }
 
 func TestCollectionSpecRewriteMaps(t *testing.T) {
@@ -177,13 +161,9 @@ func TestCollectionSpecRewriteMaps(t *testing.T) {
 	}
 
 	// Override the map with another one
-	newMap, err := NewMap(cs.Maps["test-map"])
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer newMap.Close()
+	newMap := mustNewMap(t, cs.Maps["test-map"], nil)
 
-	err = newMap.Put(uint32(0), uint32(2))
+	err := newMap.Put(uint32(0), uint32(2))
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -199,11 +179,7 @@ func TestCollectionSpecRewriteMaps(t *testing.T) {
 		t.Error("RewriteMaps doesn't remove map from CollectionSpec.Maps")
 	}
 
-	coll, err := NewCollection(cs)
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer coll.Close()
+	coll := mustNewCollection(t, cs, nil)
 
 	ret, _, err := coll.Programs["test-prog"].Test(internal.EmptyBPFContext)
 	testutils.SkipIfNotSupported(t, err)
@@ -250,26 +226,18 @@ func TestCollectionSpecMapReplacements(t *testing.T) {
 	}
 
 	// Replace the map with another one
-	newMap, err := NewMap(cs.Maps["test-map"])
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer newMap.Close()
+	newMap := mustNewMap(t, cs.Maps["test-map"], nil)
 
-	err = newMap.Put(uint32(0), uint32(2))
+	err := newMap.Put(uint32(0), uint32(2))
 	if err != nil {
 		t.Fatal(err)
 	}
 
-	coll, err := NewCollectionWithOptions(cs, CollectionOptions{
+	coll := mustNewCollection(t, cs, &CollectionOptions{
 		MapReplacements: map[string]*Map{
 			"test-map": newMap,
 		},
 	})
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer coll.Close()
 
 	ret, _, err := coll.Programs["test-prog"].Test(internal.EmptyBPFContext)
 	testutils.SkipIfNotSupported(t, err)
@@ -302,13 +270,9 @@ func TestCollectionSpecMapReplacements_NonExistingMap(t *testing.T) {
 	}
 
 	// Override non-existing map
-	newMap, err := NewMap(cs.Maps["test-map"])
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer newMap.Close()
+	newMap := mustNewMap(t, cs.Maps["test-map"], nil)
 
-	coll, err := NewCollectionWithOptions(cs, CollectionOptions{
+	coll, err := newCollection(t, cs, &CollectionOptions{
 		MapReplacements: map[string]*Map{
 			"non-existing-map": newMap,
 		},
@@ -332,19 +296,14 @@ func TestCollectionSpecMapReplacements_SpecMismatch(t *testing.T) {
 	}
 
 	// Override map with mismatching spec
-	newMap, err := NewMap(&MapSpec{
+	newMap := mustNewMap(t, &MapSpec{
 		Type:       Array,
 		KeySize:    4,
 		ValueSize:  8, // this is different
 		MaxEntries: 1,
-	})
-	if err != nil {
-		t.Fatal(err)
-	}
-	// Map fd is duplicated by MapReplacements, this one can be safely closed.
-	defer newMap.Close()
+	}, nil)
 
-	coll, err := NewCollectionWithOptions(cs, CollectionOptions{
+	coll, err := newCollection(t, cs, &CollectionOptions{
 		MapReplacements: map[string]*Map{
 			"test-map": newMap,
 		},
@@ -373,21 +332,18 @@ func TestMapReplacementsDataSections(t *testing.T) {
 		ROData *Map `ebpf:".rodata"`
 	}
 
-	err = spec.LoadAndAssign(&objs, nil)
-	testutils.SkipIfNotSupported(t, err)
-	qt.Assert(t, qt.IsNil(err))
+	mustLoadAndAssign(t, spec, &objs, nil)
 	defer objs.Data.Close()
 	defer objs.ROData.Close()
 
-	qt.Assert(t, qt.IsNil(
-		spec.LoadAndAssign(&objs, &CollectionOptions{
-			MapReplacements: map[string]*Map{
-				".data":   objs.Data,
-				".rodata": objs.ROData,
-			},
-		})))
-	defer objs.Data.Close()
-	defer objs.ROData.Close()
+	mustLoadAndAssign(t, spec, &objs, &CollectionOptions{
+		MapReplacements: map[string]*Map{
+			".data":   objs.Data,
+			".rodata": objs.ROData,
+		},
+	})
+	qt.Assert(t, qt.IsNil(objs.Data.Close()))
+	qt.Assert(t, qt.IsNil(objs.ROData.Close()))
 }
 
 func TestCollectionSpec_LoadAndAssign_LazyLoading(t *testing.T) {
@@ -429,9 +385,7 @@ func TestCollectionSpec_LoadAndAssign_LazyLoading(t *testing.T) {
 		Map  *Map     `ebpf:"valid"`
 	}
 
-	if err := spec.LoadAndAssign(&objs, nil); err != nil {
-		t.Fatal("Assign loads a map or program that isn't requested in the struct:", err)
-	}
+	mustLoadAndAssign(t, spec, &objs, nil)
 	defer objs.Prog.Close()
 	defer objs.Map.Close()
 
@@ -600,9 +554,7 @@ func TestCollectionAssign(t *testing.T) {
 		},
 	}
 
-	coll, err := NewCollection(cs)
-	qt.Assert(t, qt.IsNil(err))
-	defer coll.Close()
+	coll := mustNewCollection(t, cs, nil)
 
 	qt.Assert(t, qt.IsNil(coll.Assign(&objs)))
 	defer objs.Program.Close()
@@ -645,9 +597,7 @@ func TestCollectionAssignFail(t *testing.T) {
 		},
 	}
 
-	coll, err := NewCollection(cs)
-	qt.Assert(t, qt.IsNil(err))
-	defer coll.Close()
+	coll := mustNewCollection(t, cs, nil)
 
 	qt.Assert(t, qt.IsNotNil(coll.Assign(&objs)))
 
@@ -684,7 +634,7 @@ func TestIncompleteLoadAndAssign(t *testing.T) {
 		Invalid *Program `ebpf:"invalid"`
 	}{}
 
-	if err := spec.LoadAndAssign(&s, nil); err == nil {
+	if err := loadAndAssign(t, spec, &s, nil); err == nil {
 		t.Fatal("expected error loading invalid ProgramSpec")
 	}
 
diff --git a/elf_reader_test.go b/elf_reader_test.go
index 07711dd..6dbf51d 100644
--- a/elf_reader_test.go
+++ b/elf_reader_test.go
@@ -250,9 +250,7 @@ func TestLoadCollectionSpec(t *testing.T) {
 		qt.Assert(t, qt.Equals(have.Maps["perf_event_array"].ValueSize, 0))
 		qt.Assert(t, qt.IsNotNil(have.Maps["perf_event_array"].Value))
 
-		if diff := cmp.Diff(coll, have, csCmpOpts); diff != "" {
-			t.Errorf("MapSpec mismatch (-want +got):\n%s", diff)
-		}
+		qt.Assert(t, qt.CmpEquals(have, coll, csCmpOpts))
 
 		if have.ByteOrder != internal.NativeEndian {
 			return
diff --git a/helpers_test.go b/helpers_test.go
index 5768570..d6271b9 100644
--- a/helpers_test.go
+++ b/helpers_test.go
@@ -5,6 +5,9 @@ import (
 	"sync"
 	"testing"
 
+	"github.com/go-quicktest/qt"
+
+	"github.com/cilium/ebpf/asm"
 	"github.com/cilium/ebpf/btf"
 	"github.com/cilium/ebpf/internal/testutils"
 )
@@ -35,3 +38,144 @@ func requireTestmod(tb testing.TB) {
 		tb.Skip("bpf_testmod not loaded")
 	}
 }
+
+func newMap(tb testing.TB, spec *MapSpec, opts *MapOptions) (*Map, error) {
+	tb.Helper()
+
+	if opts == nil {
+		opts = new(MapOptions)
+	}
+
+	m, err := NewMapWithOptions(spec, *opts)
+	testutils.SkipIfNotSupportedOnOS(tb, err)
+	if err != nil {
+		return nil, err
+	}
+
+	tb.Cleanup(func() { m.Close() })
+	return m, nil
+}
+
+func mustNewMap(tb testing.TB, spec *MapSpec, opts *MapOptions) *Map {
+	tb.Helper()
+
+	m, err := newMap(tb, spec, opts)
+	qt.Assert(tb, qt.IsNil(err))
+
+	return m
+}
+
+func createMap(tb testing.TB, typ MapType, maxEntries uint32) *Map {
+	tb.Helper()
+
+	return mustNewMap(tb, &MapSpec{
+		Name:       "test",
+		Type:       typ,
+		KeySize:    4,
+		ValueSize:  4,
+		MaxEntries: maxEntries,
+	}, nil)
+}
+
+func createMapInMap(tb testing.TB, outer, inner MapType) *Map {
+	tb.Helper()
+
+	return mustNewMap(tb, &MapSpec{
+		Type:       outer,
+		KeySize:    4,
+		MaxEntries: 2,
+		InnerMap: &MapSpec{
+			Type:       inner,
+			KeySize:    4,
+			ValueSize:  4,
+			MaxEntries: 2,
+		},
+	}, nil)
+}
+
+func newProgram(tb testing.TB, spec *ProgramSpec, opts *ProgramOptions) (*Program, error) {
+	tb.Helper()
+
+	if opts == nil {
+		opts = new(ProgramOptions)
+	}
+
+	prog, err := NewProgramWithOptions(spec, *opts)
+	testutils.SkipIfNotSupportedOnOS(tb, err)
+	if err != nil {
+		return nil, err
+	}
+
+	tb.Cleanup(func() { prog.Close() })
+	return prog, nil
+}
+
+func mustNewProgram(tb testing.TB, spec *ProgramSpec, opts *ProgramOptions) *Program {
+	prog, err := newProgram(tb, spec, opts)
+	qt.Assert(tb, qt.IsNil(err))
+	return prog
+}
+
+func createProgram(tb testing.TB, typ ProgramType, retval int64) *Program {
+	tb.Helper()
+
+	return mustNewProgram(tb, &ProgramSpec{
+		Name: "test",
+		Type: typ,
+		Instructions: asm.Instructions{
+			asm.LoadImm(asm.R0, retval, asm.DWord),
+			asm.Return(),
+		},
+		License: "MIT",
+	}, nil)
+}
+
+var basicProgramSpec = &ProgramSpec{
+	Name: "test",
+	Type: SocketFilter,
+	Instructions: asm.Instructions{
+		asm.LoadImm(asm.R0, 2, asm.DWord),
+		asm.Return(),
+	},
+	License: "MIT",
+}
+
+// createBasicProgram returns a program of an unspecified type which returns
+// a non-zero value when executed.
+func createBasicProgram(tb testing.TB) *Program {
+	return mustNewProgram(tb, basicProgramSpec, nil)
+}
+
+func newCollection(tb testing.TB, spec *CollectionSpec, opts *CollectionOptions) (*Collection, error) {
+	tb.Helper()
+
+	if opts == nil {
+		opts = new(CollectionOptions)
+	}
+
+	c, err := NewCollectionWithOptions(spec, *opts)
+	if err != nil {
+		return nil, err
+	}
+
+	tb.Cleanup(func() { c.Close() })
+	return c, nil
+}
+
+func mustNewCollection(tb testing.TB, spec *CollectionSpec, opts *CollectionOptions) *Collection {
+	tb.Helper()
+	c, err := newCollection(tb, spec, opts)
+	qt.Assert(tb, qt.IsNil(err))
+	return c
+}
+
+func loadAndAssign(tb testing.TB, spec *CollectionSpec, to any, opts *CollectionOptions) error {
+	tb.Helper()
+	err := spec.LoadAndAssign(to, opts)
+	testutils.SkipIfNotSupported(tb, err)
+	return err
+}
+
+func mustLoadAndAssign(tb testing.TB, spec *CollectionSpec, to any, opts *CollectionOptions) {
+	qt.Assert(tb, qt.IsNil(loadAndAssign(tb, spec, to, opts)))
+}
diff --git a/info_test.go b/info_test.go
index 7be0ea8..febdf44 100644
--- a/info_test.go
+++ b/info_test.go
@@ -43,21 +43,16 @@ var multiprogSpec = &ProgramSpec{
 }
 
 func TestMapInfoFromProc(t *testing.T) {
-	hash, err := NewMap(&MapSpec{
+	hash := mustNewMap(t, &MapSpec{
 		Type:       Hash,
 		KeySize:    4,
 		ValueSize:  5,
 		MaxEntries: 2,
 		Flags:      sys.BPF_F_NO_PREALLOC,
-	})
-	testutils.SkipIfNotSupported(t, err)
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer hash.Close()
+	}, nil)
 
 	var info MapInfo
-	err = readMapInfoFromProc(hash.fd, &info)
+	err := readMapInfoFromProc(hash.fd, &info)
 	testutils.SkipIfNotSupported(t, err)
 
 	qt.Assert(t, qt.IsNil(err))
@@ -69,7 +64,7 @@ func TestMapInfoFromProc(t *testing.T) {
 }
 
 func TestMapInfoFromProcOuterMap(t *testing.T) {
-	outer, err := NewMap(&MapSpec{
+	outer := mustNewMap(t, &MapSpec{
 		Type:       ArrayOfMaps,
 		KeySize:    4,
 		MaxEntries: 2,
@@ -79,15 +74,10 @@ func TestMapInfoFromProcOuterMap(t *testing.T) {
 			ValueSize:  4,
 			MaxEntries: 2,
 		},
-	})
-	testutils.SkipIfNotSupported(t, err)
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer outer.Close()
+	}, nil)
 
 	var info MapInfo
-	err = readMapInfoFromProc(outer.fd, &info)
+	err := readMapInfoFromProc(outer.fd, &info)
 	testutils.SkipIfNotSupported(t, err)
 
 	qt.Assert(t, qt.IsNil(err))
@@ -103,7 +93,7 @@ func validateProgInfo(t *testing.T, info *ProgramInfo) {
 }
 
 func TestProgramInfo(t *testing.T) {
-	prog := mustSocketFilter(t)
+	prog := createBasicProgram(t)
 
 	info, err := newProgramInfoFromFd(prog.fd)
 	testutils.SkipIfNotSupported(t, err)
@@ -165,7 +155,7 @@ func TestProgramInfo(t *testing.T) {
 }
 
 func TestProgramInfoProc(t *testing.T) {
-	prog := mustSocketFilter(t)
+	prog := createBasicProgram(t)
 
 	info, err := newProgramInfoFromProc(prog.fd)
 	testutils.SkipIfNotSupported(t, err)
@@ -175,10 +165,9 @@ func TestProgramInfoProc(t *testing.T) {
 }
 
 func TestProgramInfoBTF(t *testing.T) {
-	prog, err := NewProgram(multiprogSpec)
+	prog, err := newProgram(t, multiprogSpec, nil)
 	testutils.SkipIfNotSupported(t, err)
 	qt.Assert(t, qt.IsNil(err))
-	t.Cleanup(func() { prog.Close() })
 
 	info, err := prog.Info()
 	testutils.SkipIfNotSupported(t, err)
@@ -227,16 +216,9 @@ func TestProgramInfoBTF(t *testing.T) {
 }
 
 func TestProgramInfoMapIDs(t *testing.T) {
-	arr, err := NewMap(&MapSpec{
-		Type:       Array,
-		KeySize:    4,
-		ValueSize:  4,
-		MaxEntries: 1,
-	})
-	qt.Assert(t, qt.IsNil(err))
-	defer arr.Close()
+	arr := createMap(t, Array, 1)
 
-	prog, err := NewProgram(&ProgramSpec{
+	prog := mustNewProgram(t, &ProgramSpec{
 		Type: SocketFilter,
 		Instructions: asm.Instructions{
 			asm.LoadMapPtr(asm.R0, arr.FD()),
@@ -244,9 +226,7 @@ func TestProgramInfoMapIDs(t *testing.T) {
 			asm.Return(),
 		},
 		License: "MIT",
-	})
-	qt.Assert(t, qt.IsNil(err))
-	defer prog.Close()
+	}, nil)
 
 	info, err := prog.Info()
 	testutils.SkipIfNotSupported(t, err)
@@ -271,16 +251,7 @@ func TestProgramInfoMapIDs(t *testing.T) {
 }
 
 func TestProgramInfoMapIDsNoMaps(t *testing.T) {
-	prog, err := NewProgram(&ProgramSpec{
-		Type: SocketFilter,
-		Instructions: asm.Instructions{
-			asm.LoadImm(asm.R0, 0, asm.DWord),
-			asm.Return(),
-		},
-		License: "MIT",
-	})
-	qt.Assert(t, qt.IsNil(err))
-	defer prog.Close()
+	prog := createBasicProgram(t)
 
 	info, err := prog.Info()
 	testutils.SkipIfNotSupported(t, err)
@@ -328,7 +299,7 @@ func TestScanFdInfoReader(t *testing.T) {
 func TestStats(t *testing.T) {
 	testutils.SkipOnOldKernel(t, "5.8", "BPF_ENABLE_STATS")
 
-	prog := mustSocketFilter(t)
+	prog := createBasicProgram(t)
 
 	pi, err := prog.Info()
 	if err != nil {
@@ -368,7 +339,7 @@ func TestStats(t *testing.T) {
 func BenchmarkStats(b *testing.B) {
 	testutils.SkipOnOldKernel(b, "5.8", "BPF_ENABLE_STATS")
 
-	prog := mustSocketFilter(b)
+	prog := createBasicProgram(b)
 
 	for n := 0; n < b.N; n++ {
 		if err := testStats(prog); err != nil {
diff --git a/linker_test.go b/linker_test.go
index cbe31d6..bd387a7 100644
--- a/linker_test.go
+++ b/linker_test.go
@@ -41,12 +41,9 @@ func TestFindReferences(t *testing.T) {
 
 	flattenPrograms(progs, []string{"entrypoint"})
 
-	prog, err := NewProgram(progs["entrypoint"])
+	prog, err := newProgram(t, progs["entrypoint"], nil)
 	testutils.SkipIfNotSupported(t, err)
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer prog.Close()
+	qt.Assert(t, qt.IsNil(err))
 
 	ret, _, err := prog.Test(internal.EmptyBPFContext)
 	if err != nil {
@@ -68,7 +65,7 @@ func TestForwardFunctionDeclaration(t *testing.T) {
 	spec := coll.Programs["call_fwd"]
 
 	// This program calls an unimplemented forward function declaration.
-	_, err = NewProgram(spec)
+	_, err = newProgram(t, spec, nil)
 	if !errors.Is(err, asm.ErrUnsatisfiedProgramReference) {
 		t.Fatal("Expected an error wrapping ErrUnsatisfiedProgramReference, got:", err)
 	}
@@ -90,12 +87,9 @@ func TestForwardFunctionDeclaration(t *testing.T) {
 		}
 	}
 
-	prog, err := NewProgram(spec)
+	prog, err := newProgram(t, spec, nil)
 	testutils.SkipIfNotSupported(t, err)
-	if err != nil {
-		t.Fatalf("%+v", err)
-	}
-	defer prog.Close()
+	qt.Assert(t, qt.IsNil(err))
 
 	ret, _, err := prog.Test(internal.EmptyBPFContext)
 	if err != nil {
diff --git a/map_test.go b/map_test.go
index 56e65b9..6f4f1e9 100644
--- a/map_test.go
+++ b/map_test.go
@@ -7,6 +7,7 @@ import (
 	"math"
 	"os"
 	"path/filepath"
+	"slices"
 	"sort"
 	"testing"
 	"unsafe"
@@ -32,23 +33,8 @@ var (
 	}
 )
 
-// newHash returns a new Map of type Hash. Cleanup is handled automatically.
-func newHash(t *testing.T) *Map {
-	hash, err := NewMap(&MapSpec{
-		Type:       Hash,
-		KeySize:    5,
-		ValueSize:  4,
-		MaxEntries: 10,
-	})
-	if err != nil {
-		t.Fatal(err)
-	}
-	t.Cleanup(func() { hash.Close() })
-	return hash
-}
-
 func TestMap(t *testing.T) {
-	m := createArray(t)
+	m := createMap(t, Array, 2)
 
 	t.Log(m)
 
@@ -124,20 +110,6 @@ func TestMapBatch(t *testing.T) {
 		42, 4242, 23, 2323,
 	}
 
-	mustNewMap := func(t *testing.T, mapType MapType, max uint32) *Map {
-		m, err := NewMap(&MapSpec{
-			Type:       mapType,
-			KeySize:    4,
-			ValueSize:  4,
-			MaxEntries: max,
-		})
-		if err != nil {
-			t.Fatal(err)
-		}
-		t.Cleanup(func() { m.Close() })
-		return m
-	}
-
 	keysAndValuesForMap := func(m *Map, contents []uint32) (keys, values []uint32, stride int) {
 		possibleCPU := 1
 		if m.Type().hasPerCPUValue() {
@@ -163,7 +135,7 @@ func TestMapBatch(t *testing.T) {
 				testutils.SkipOnOldKernel(t, "5.13", "batched ops support for percpu array")
 			}
 
-			m := mustNewMap(t, typ, uint32(len(contents)))
+			m := createMap(t, typ, uint32(len(contents)))
 			keys, values, _ := keysAndValuesForMap(m, contents)
 			count, err := m.BatchUpdate(keys, values, nil)
 			qt.Assert(t, qt.IsNil(err))
@@ -187,7 +159,7 @@ func TestMapBatch(t *testing.T) {
 
 	for _, typ := range []MapType{Hash, PerCPUHash} {
 		t.Run(typ.String(), func(t *testing.T) {
-			m := mustNewMap(t, typ, uint32(len(contents)))
+			m := createMap(t, typ, uint32(len(contents)))
 			keys, values, stride := keysAndValuesForMap(m, contents)
 			count, err := m.BatchUpdate(keys, values, nil)
 			qt.Assert(t, qt.IsNil(err))
@@ -229,29 +201,13 @@ func TestMapBatch(t *testing.T) {
 }
 
 func TestMapBatchCursorReuse(t *testing.T) {
-	spec := &MapSpec{
-		Type:       Array,
-		KeySize:    4,
-		ValueSize:  4,
-		MaxEntries: 4,
-	}
-
-	arr1, err := NewMap(spec)
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer arr1.Close()
-
-	arr2, err := NewMap(spec)
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer arr2.Close()
+	arr1 := createMap(t, Array, 4)
+	arr2 := createMap(t, Array, 4)
 
 	tmp := make([]uint32, 2)
 
 	var cursor MapBatchCursor
-	_, err = arr1.BatchLookup(&cursor, tmp, tmp, nil)
+	_, err := arr1.BatchLookup(&cursor, tmp, tmp, nil)
 	testutils.SkipIfNotSupported(t, err)
 	qt.Assert(t, qt.IsNil(err))
 
@@ -260,7 +216,7 @@ func TestMapBatchCursorReuse(t *testing.T) {
 }
 
 func TestMapLookupKeyTooSmall(t *testing.T) {
-	m := createArray(t)
+	m := createMap(t, Array, 2)
 	defer m.Close()
 
 	var small uint16
@@ -269,7 +225,7 @@ func TestMapLookupKeyTooSmall(t *testing.T) {
 }
 
 func TestMapLookupKeyNotFoundAllocations(t *testing.T) {
-	m := createArray(t)
+	m := createMap(t, Array, 2)
 	defer m.Close()
 	var key, out uint32 = 3, 0
 
@@ -283,16 +239,8 @@ func TestBatchAPIMapDelete(t *testing.T) {
 	if err := haveBatchAPI(); err != nil {
 		t.Skipf("batch api not available: %v", err)
 	}
-	m, err := NewMap(&MapSpec{
-		Type:       Hash,
-		KeySize:    4,
-		ValueSize:  4,
-		MaxEntries: 10,
-	})
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer m.Close()
+
+	m := createMap(t, Hash, 10)
 
 	var (
 		keys   = []uint32{0, 1}
@@ -329,7 +277,7 @@ func TestBatchAPIMapDelete(t *testing.T) {
 }
 
 func TestMapClose(t *testing.T) {
-	m := createArray(t)
+	m := createMap(t, Array, 2)
 
 	if err := m.Close(); err != nil {
 		t.Fatal("Can't close map:", err)
@@ -472,7 +420,7 @@ func TestMapCloneNil(t *testing.T) {
 }
 
 func TestMapPin(t *testing.T) {
-	m := createArray(t)
+	m := createMap(t, Array, 2)
 
 	if err := m.Put(uint32(0), uint32(42)); err != nil {
 		t.Fatal("Can't put:", err)
@@ -508,23 +456,7 @@ func TestMapPin(t *testing.T) {
 }
 
 func TestNestedMapPin(t *testing.T) {
-	m, err := NewMap(&MapSpec{
-		Type:       ArrayOfMaps,
-		KeySize:    4,
-		ValueSize:  4,
-		MaxEntries: 2,
-		InnerMap: &MapSpec{
-			Type:       Array,
-			KeySize:    4,
-			ValueSize:  4,
-			MaxEntries: 1,
-		},
-	})
-	testutils.SkipIfNotSupported(t, err)
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer m.Close()
+	m := createMapInMap(t, ArrayOfMaps, Array)
 
 	tmp, err := os.MkdirTemp("/sys/fs/bpf", "ebpf-test")
 	if err != nil {
@@ -547,7 +479,7 @@ func TestNestedMapPin(t *testing.T) {
 }
 
 func TestNestedMapPinNested(t *testing.T) {
-	if _, err := NewMap(&MapSpec{
+	if _, err := newMap(t, &MapSpec{
 		Type:       ArrayOfMaps,
 		KeySize:    4,
 		ValueSize:  4,
@@ -560,7 +492,7 @@ func TestNestedMapPinNested(t *testing.T) {
 			MaxEntries: 1,
 			Pinning:    PinByName,
 		},
-	}); err == nil {
+	}, nil); err == nil {
 		t.Error("Inner maps should not be pinnable")
 	}
 }
@@ -572,16 +504,12 @@ func TestMapPinMultiple(t *testing.T) {
 
 	spec := spec1.Copy()
 
-	m1, err := NewMapWithOptions(spec, MapOptions{PinPath: tmp})
-	if err != nil {
-		t.Fatal("Can't create map:", err)
-	}
-	defer m1.Close()
+	m1 := mustNewMap(t, spec, &MapOptions{PinPath: tmp})
 	pinned := m1.IsPinned()
 	qt.Assert(t, qt.IsTrue(pinned))
 
 	newPath := filepath.Join(tmp, "bar")
-	err = m1.Pin(newPath)
+	err := m1.Pin(newPath)
 	testutils.SkipIfNotSupported(t, err)
 	qt.Assert(t, qt.IsNil(err))
 	oldPath := filepath.Join(tmp, spec.Name)
@@ -596,7 +524,7 @@ func TestMapPinMultiple(t *testing.T) {
 }
 
 func TestMapPinWithEmptyPath(t *testing.T) {
-	m := createArray(t)
+	m := createMap(t, Array, 2)
 
 	err := m.Pin("")
 
@@ -609,16 +537,9 @@ func TestMapPinFailReplace(t *testing.T) {
 	spec2 := spec1.Copy()
 	spec2.Name = spec1.Name + "bar"
 
-	m, err := NewMapWithOptions(spec, MapOptions{PinPath: tmp})
-	if err != nil {
-		t.Fatal("Failed to create map:", err)
-	}
-	defer m.Close()
-	m2, err := NewMapWithOptions(spec2, MapOptions{PinPath: tmp})
-	if err != nil {
-		t.Fatal("Failed to create map2:", err)
-	}
-	defer m2.Close()
+	m := mustNewMap(t, spec, &MapOptions{PinPath: tmp})
+	_ = mustNewMap(t, spec2, &MapOptions{PinPath: tmp})
+
 	qt.Assert(t, qt.IsTrue(m.IsPinned()))
 	newPath := filepath.Join(tmp, spec2.Name)
 
@@ -630,11 +551,7 @@ func TestMapUnpin(t *testing.T) {
 	tmp := testutils.TempBPFFS(t)
 	spec := spec1.Copy()
 
-	m, err := NewMapWithOptions(spec, MapOptions{PinPath: tmp})
-	if err != nil {
-		t.Fatal("Failed to create map:", err)
-	}
-	defer m.Close()
+	m := mustNewMap(t, spec, &MapOptions{PinPath: tmp})
 
 	pinned := m.IsPinned()
 	qt.Assert(t, qt.IsTrue(pinned))
@@ -657,9 +574,7 @@ func TestMapLoadPinned(t *testing.T) {
 
 	spec := spec1.Copy()
 
-	m1, err := NewMapWithOptions(spec, MapOptions{PinPath: tmp})
-	qt.Assert(t, qt.IsNil(err))
-	defer m1.Close()
+	m1 := mustNewMap(t, spec, &MapOptions{PinPath: tmp})
 	pinned := m1.IsPinned()
 	qt.Assert(t, qt.IsTrue(pinned))
 
@@ -691,13 +606,8 @@ func TestMapLoadReusePinned(t *testing.T) {
 				Pinning:    PinByName,
 			}
 
-			m1, err := NewMapWithOptions(spec, MapOptions{PinPath: tmp})
-			qt.Assert(t, qt.IsNil(err))
-			defer m1.Close()
-
-			m2, err := NewMapWithOptions(spec, MapOptions{PinPath: tmp})
-			qt.Assert(t, qt.IsNil(err))
-			defer m2.Close()
+			_ = mustNewMap(t, spec, &MapOptions{PinPath: tmp})
+			_ = mustNewMap(t, spec, &MapOptions{PinPath: tmp})
 		})
 	}
 }
@@ -707,9 +617,7 @@ func TestMapLoadPinnedUnpin(t *testing.T) {
 
 	spec := spec1.Copy()
 
-	m1, err := NewMapWithOptions(spec, MapOptions{PinPath: tmp})
-	qt.Assert(t, qt.IsNil(err))
-	defer m1.Close()
+	m1 := mustNewMap(t, spec, &MapOptions{PinPath: tmp})
 	pinned := m1.IsPinned()
 	qt.Assert(t, qt.IsTrue(pinned))
 
@@ -728,7 +636,7 @@ func TestMapLoadPinnedWithOptions(t *testing.T) {
 	// Introduced in commit 6e71b04a8224.
 	testutils.SkipOnOldKernel(t, "4.15", "file_flags in BPF_OBJ_GET")
 
-	array := createArray(t)
+	array := createMap(t, Array, 2)
 
 	tmp := testutils.TempBPFFS(t)
 
@@ -785,13 +693,9 @@ func TestMapPinFlags(t *testing.T) {
 		Pinning:    PinByName,
 	}
 
-	m, err := NewMapWithOptions(spec, MapOptions{
-		PinPath: tmp,
-	})
-	qt.Assert(t, qt.IsNil(err))
-	m.Close()
+	_ = mustNewMap(t, spec, &MapOptions{PinPath: tmp})
 
-	_, err = NewMapWithOptions(spec, MapOptions{
+	_, err := newMap(t, spec, &MapOptions{
 		PinPath: tmp,
 		LoadPinOptions: LoadPinOptions{
 			Flags: math.MaxUint32,
@@ -802,34 +706,14 @@ func TestMapPinFlags(t *testing.T) {
 	}
 }
 
-func createArray(t *testing.T) *Map {
-	t.Helper()
-
-	m, err := NewMap(&MapSpec{
-		Type:       Array,
-		KeySize:    4,
-		ValueSize:  4,
-		MaxEntries: 2,
-	})
-	if err != nil {
-		t.Fatal(err)
-	}
-	t.Cleanup(func() { m.Close() })
-	return m
-}
-
 func TestMapQueue(t *testing.T) {
 	testutils.SkipOnOldKernel(t, "4.20", "map type queue")
 
-	m, err := NewMap(&MapSpec{
+	m := mustNewMap(t, &MapSpec{
 		Type:       Queue,
 		ValueSize:  4,
 		MaxEntries: 2,
-	})
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer m.Close()
+	}, nil)
 
 	for _, v := range []uint32{42, 4242} {
 		if err := m.Put(nil, v); err != nil {
@@ -873,34 +757,12 @@ func TestMapQueue(t *testing.T) {
 func TestMapInMap(t *testing.T) {
 	for _, typ := range []MapType{ArrayOfMaps, HashOfMaps} {
 		t.Run(typ.String(), func(t *testing.T) {
-			spec := &MapSpec{
-				Type:       typ,
-				KeySize:    4,
-				MaxEntries: 2,
-				InnerMap: &MapSpec{
-					Type:       Array,
-					KeySize:    4,
-					ValueSize:  4,
-					MaxEntries: 2,
-				},
-			}
-
-			inner, err := NewMap(spec.InnerMap)
-			if err != nil {
-				t.Fatal(err)
-			}
+			inner := createMap(t, Array, 2)
 			if err := inner.Put(uint32(1), uint32(4242)); err != nil {
 				t.Fatal(err)
 			}
-			defer inner.Close()
-
-			outer, err := NewMap(spec)
-			testutils.SkipIfNotSupported(t, err)
-			if err != nil {
-				t.Fatal(err)
-			}
-			defer outer.Close()
 
+			outer := createMapInMap(t, typ, Array)
 			if err := outer.Put(uint32(0), inner); err != nil {
 				t.Fatal("Can't put inner map:", err)
 			}
@@ -939,22 +801,7 @@ func TestMapInMap(t *testing.T) {
 }
 
 func TestNewMapInMapFromFD(t *testing.T) {
-	nested, err := NewMap(&MapSpec{
-		Type:       ArrayOfMaps,
-		KeySize:    4,
-		MaxEntries: 2,
-		InnerMap: &MapSpec{
-			Type:       Array,
-			KeySize:    4,
-			ValueSize:  4,
-			MaxEntries: 2,
-		},
-	})
-	testutils.SkipIfNotSupported(t, err)
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer nested.Close()
+	nested := createMapInMap(t, ArrayOfMaps, Array)
 
 	// Do not copy this, use Clone instead.
 	another, err := NewMapFromFD(dupFD(t, nested.FD()))
@@ -972,45 +819,15 @@ func TestPerfEventArray(t *testing.T) {
 	}
 
 	for _, spec := range specs {
-		m, err := NewMap(spec)
-		if err != nil {
-			t.Errorf("Can't create perf event array from %v: %s", spec, err)
-		} else {
-			m.Close()
-		}
+		_ = mustNewMap(t, spec, nil)
 	}
 }
 
 func TestCPUMap(t *testing.T) {
 	testutils.SkipOnOldKernel(t, "4.15", "cpu map")
 
-	m, err := NewMap(&MapSpec{Type: CPUMap, KeySize: 4, ValueSize: 4})
-	qt.Assert(t, qt.IsNil(err))
+	m := mustNewMap(t, &MapSpec{Type: CPUMap, KeySize: 4, ValueSize: 4}, nil)
 	qt.Assert(t, qt.Equals(m.MaxEntries(), uint32(MustPossibleCPU())))
-	m.Close()
-}
-
-func createMapInMap(t *testing.T, typ MapType) *Map {
-	t.Helper()
-
-	spec := &MapSpec{
-		Type:       typ,
-		KeySize:    4,
-		MaxEntries: 2,
-		InnerMap: &MapSpec{
-			Type:       Array,
-			KeySize:    4,
-			ValueSize:  4,
-			MaxEntries: 2,
-		},
-	}
-
-	m, err := NewMap(spec)
-	testutils.SkipIfNotSupported(t, err)
-	if err != nil {
-		t.Fatal(err)
-	}
-	return m
 }
 
 func TestMapInMapValueSize(t *testing.T) {
@@ -1027,41 +844,28 @@ func TestMapInMapValueSize(t *testing.T) {
 		},
 	}
 
-	m, err := NewMap(spec)
-	testutils.SkipIfNotSupported(t, err)
-	if err != nil {
-		t.Fatal(err)
-	}
-	m.Close()
+	_ = mustNewMap(t, spec, nil)
 
 	spec.ValueSize = 4
-	m, err = NewMap(spec)
-	if err != nil {
-		t.Fatal(err)
-	}
-	m.Close()
+	_ = mustNewMap(t, spec, nil)
 
 	spec.ValueSize = 1
-	if _, err := NewMap(spec); err == nil {
-		t.Fatal("Expected an error")
-	}
+	_, err := newMap(t, spec, nil)
+	qt.Assert(t, qt.IsNotNil(err))
 }
 
 func TestIterateEmptyMap(t *testing.T) {
 	makeMap := func(t *testing.T, mapType MapType) *Map {
-		m, err := NewMap(&MapSpec{
+		m, err := newMap(t, &MapSpec{
 			Type:       mapType,
 			KeySize:    4,
 			ValueSize:  8,
 			MaxEntries: 2,
-		})
+		}, nil)
 		if errors.Is(err, unix.EINVAL) {
 			t.Skip(mapType, "is not supported")
 		}
-		if err != nil {
-			t.Fatal("Can't create map:", err)
-		}
-		t.Cleanup(func() { m.Close() })
+		qt.Assert(t, qt.IsNil(err))
 		return m
 	}
 
@@ -1104,18 +908,10 @@ func TestIterateEmptyMap(t *testing.T) {
 }
 
 func TestMapIterate(t *testing.T) {
-	hash, err := NewMap(&MapSpec{
-		Type:       Hash,
-		KeySize:    5,
-		ValueSize:  4,
-		MaxEntries: 2,
-	})
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer hash.Close()
+	hash := createMap(t, Hash, 2)
 
-	data := []string{"hello", "world"}
+	data := []string{"test", "more"}
+	slices.Sort(data)
 	for i, k := range data {
 		if err := hash.Put(k, uint32(i)); err != nil {
 			t.Fatal(err)
@@ -1139,7 +935,7 @@ func TestMapIterate(t *testing.T) {
 func TestIterateWrongMap(t *testing.T) {
 	testutils.SkipOnOldKernel(t, "4.20", "map type queue")
 
-	m, err := NewMap(&MapSpec{
+	m := mustNewMap(t, &MapSpec{
 		Type:       Queue,
 		ValueSize:  4,
 		MaxEntries: 2,
@@ -1147,9 +943,7 @@ func TestIterateWrongMap(t *testing.T) {
 			{nil, uint32(0)},
 			{nil, uint32(1)},
 		},
-	})
-	qt.Assert(t, qt.IsNil(err))
-	defer m.Close()
+	}, nil)
 
 	var value uint32
 	entries := m.Iterate()
@@ -1159,16 +953,7 @@ func TestIterateWrongMap(t *testing.T) {
 }
 
 func TestMapIteratorAllocations(t *testing.T) {
-	arr, err := NewMap(&MapSpec{
-		Type:       Array,
-		KeySize:    4,
-		ValueSize:  4,
-		MaxEntries: 10,
-	})
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer arr.Close()
+	arr := createMap(t, Array, 10)
 
 	var k, v uint32
 	iter := arr.Iterate()
@@ -1186,16 +971,7 @@ func TestMapIteratorAllocations(t *testing.T) {
 func TestMapBatchLookupAllocations(t *testing.T) {
 	testutils.SkipIfNotSupported(t, haveBatchAPI())
 
-	arr, err := NewMap(&MapSpec{
-		Type:       Array,
-		KeySize:    4,
-		ValueSize:  4,
-		MaxEntries: 10,
-	})
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer arr.Close()
+	arr := createMap(t, Array, 10)
 
 	var cursor MapBatchCursor
 	tmp := make([]uint32, 2)
@@ -1213,16 +989,12 @@ func TestMapBatchLookupAllocations(t *testing.T) {
 }
 
 func TestMapIterateHashKeyOneByteFull(t *testing.T) {
-	hash, err := NewMap(&MapSpec{
+	hash := mustNewMap(t, &MapSpec{
 		Type:       Hash,
 		KeySize:    1,
 		ValueSize:  1,
 		MaxEntries: 256,
-	})
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer hash.Close()
+	}, nil)
 
 	for i := 0; i < int(hash.MaxEntries()); i++ {
 		if err := hash.Put(uint8(i), uint8(i)); err != nil {
@@ -1289,16 +1061,12 @@ func TestMapGuessNonExistentKey(t *testing.T) {
 				maxEntries = 1
 			}
 
-			m, err := NewMap(&MapSpec{
+			m := mustNewMap(t, &MapSpec{
 				Type:       tt.mapType,
 				KeySize:    4,
 				ValueSize:  4,
 				MaxEntries: maxEntries,
-			})
-			if err != nil {
-				t.Fatal(err)
-			}
-			defer m.Close()
+			}, nil)
 
 			for _, key := range tt.keys {
 				if err := m.Put(key, key); err != nil {
@@ -1325,16 +1093,12 @@ func TestMapGuessNonExistentKey(t *testing.T) {
 	t.Run("Hash: full", func(t *testing.T) {
 		const n = math.MaxUint8 + 1
 
-		hash, err := NewMap(&MapSpec{
+		hash := mustNewMap(t, &MapSpec{
 			Type:       Hash,
 			KeySize:    1,
 			ValueSize:  1,
 			MaxEntries: n,
-		})
-		if err != nil {
-			t.Fatal(err)
-		}
-		defer hash.Close()
+		}, nil)
 
 		for i := 0; i < n; i++ {
 			if err := hash.Put(uint8(i), uint8(i)); err != nil {
@@ -1342,7 +1106,7 @@ func TestMapGuessNonExistentKey(t *testing.T) {
 			}
 		}
 
-		_, err = hash.guessNonExistentKey()
+		_, err := hash.guessNonExistentKey()
 		if err == nil {
 			t.Fatal("guessNonExistentKey doesn't return error on full hash table")
 		}
@@ -1350,15 +1114,15 @@ func TestMapGuessNonExistentKey(t *testing.T) {
 }
 
 func TestNotExist(t *testing.T) {
-	hash := newHash(t)
+	hash := createMap(t, Hash, 10)
 
 	var tmp uint32
-	err := hash.Lookup("hello", &tmp)
+	err := hash.Lookup("test", &tmp)
 	if !errors.Is(err, ErrKeyNotExist) {
 		t.Error("Lookup doesn't return ErrKeyNotExist")
 	}
 
-	buf, err := hash.LookupBytes("hello")
+	buf, err := hash.LookupBytes("test")
 	if err != nil {
 		t.Error("Looking up non-existent key return an error:", err)
 	}
@@ -1366,11 +1130,11 @@ func TestNotExist(t *testing.T) {
 		t.Error("LookupBytes returns non-nil buffer for non-existent key")
 	}
 
-	if err := hash.Delete("hello"); !errors.Is(err, ErrKeyNotExist) {
+	if err := hash.Delete("test"); !errors.Is(err, ErrKeyNotExist) {
 		t.Error("Deleting unknown key doesn't return ErrKeyNotExist", err)
 	}
 
-	var k = []byte{1, 2, 3, 4, 5}
+	var k = []byte{1, 2, 3, 4}
 	if err := hash.NextKey(&k, &tmp); !errors.Is(err, ErrKeyNotExist) {
 		t.Error("Looking up next key in empty map doesn't return a non-existing error", err)
 	}
@@ -1381,13 +1145,13 @@ func TestNotExist(t *testing.T) {
 }
 
 func TestExist(t *testing.T) {
-	hash := newHash(t)
+	hash := createMap(t, Hash, 10)
 
-	if err := hash.Put("hello", uint32(21)); err != nil {
+	if err := hash.Put("test", uint32(21)); err != nil {
 		t.Errorf("Failed to put key/value pair into hash: %v", err)
 	}
 
-	if err := hash.Update("hello", uint32(42), UpdateNoExist); !errors.Is(err, ErrKeyExist) {
+	if err := hash.Update("test", uint32(42), UpdateNoExist); !errors.Is(err, ErrKeyExist) {
 		t.Error("Updating existing key doesn't return ErrKeyExist")
 	}
 }
@@ -1395,10 +1159,10 @@ func TestExist(t *testing.T) {
 func TestIterateMapInMap(t *testing.T) {
 	const idx = uint32(1)
 
-	parent := createMapInMap(t, ArrayOfMaps)
+	parent := createMapInMap(t, ArrayOfMaps, Array)
 	defer parent.Close()
 
-	a := createArray(t)
+	a := createMap(t, Array, 2)
 
 	if err := parent.Put(idx, a); err != nil {
 		t.Fatal(err)
@@ -1438,20 +1202,11 @@ func TestPerCPUMarshaling(t *testing.T) {
 				testutils.SkipOnOldKernel(t, "4.10", "LRU per-CPU hash")
 			}
 
-			arr, err := NewMap(&MapSpec{
-				Type:       typ,
-				KeySize:    4,
-				ValueSize:  5,
-				MaxEntries: 1,
-			})
-			if err != nil {
-				t.Fatal(err)
-			}
-			defer arr.Close()
+			arr := createMap(t, typ, 1)
 
 			values := []*customEncoding{
-				{"hello"},
-				{"world"},
+				{"test"},
+				{"more"},
 			}
 			if err := arr.Put(uint32(0), values); err != nil {
 				t.Fatal(err)
@@ -1473,7 +1228,7 @@ func TestPerCPUMarshaling(t *testing.T) {
 				t.Fatal("Can't retrieve key 0:", err)
 			}
 
-			for i, want := range []string{"HELLO", "WORLD"} {
+			for i, want := range []string{"TEST", "MORE"} {
 				if retrieved[i] == nil {
 					t.Error("First item is nil")
 				} else if have := retrieved[i].data; have != want {
@@ -1500,19 +1255,13 @@ func TestCgroupPerCPUStorageMarshaling(t *testing.T) {
 
 	cgroup := testutils.CreateCgroup(t)
 
-	arr, err := NewMap(&MapSpec{
+	arr := mustNewMap(t, &MapSpec{
 		Type:      PerCPUCGroupStorage,
 		KeySize:   uint32(unsafe.Sizeof(bpfCgroupStorageKey{})),
 		ValueSize: uint32(unsafe.Sizeof(uint64(0))),
-	})
-	if err != nil {
-		t.Fatal(err)
-	}
-	t.Cleanup(func() {
-		arr.Close()
-	})
+	}, nil)
 
-	prog, err := NewProgram(&ProgramSpec{
+	prog := mustNewProgram(t, &ProgramSpec{
 		Type:       CGroupSKB,
 		AttachType: AttachCGroupInetEgress,
 		License:    "MIT",
@@ -1523,11 +1272,7 @@ func TestCgroupPerCPUStorageMarshaling(t *testing.T) {
 			asm.Mov.Imm(asm.R0, 0),
 			asm.Return(),
 		},
-	})
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer prog.Close()
+	}, nil)
 
 	progAttachAttrs := sys.ProgAttachAttr{
 		TargetFdOrIfindex: uint32(cgroup.Fd()),
@@ -1536,7 +1281,7 @@ func TestCgroupPerCPUStorageMarshaling(t *testing.T) {
 		AttachFlags:       0,
 		ReplaceBpfFd:      0,
 	}
-	err = sys.ProgAttach(&progAttachAttrs)
+	err := sys.ProgAttach(&progAttachAttrs)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -1576,16 +1321,7 @@ func TestCgroupPerCPUStorageMarshaling(t *testing.T) {
 }
 
 func TestMapMarshalUnsafe(t *testing.T) {
-	m, err := NewMap(&MapSpec{
-		Type:       Hash,
-		KeySize:    4,
-		ValueSize:  4,
-		MaxEntries: 1,
-	})
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer m.Close()
+	m := createMap(t, Hash, 1)
 
 	key := uint32(1)
 	value := uint32(42)
@@ -1631,17 +1367,7 @@ func TestMapName(t *testing.T) {
 		t.Skip(err)
 	}
 
-	m, err := NewMap(&MapSpec{
-		Name:       "test",
-		Type:       Array,
-		KeySize:    4,
-		ValueSize:  4,
-		MaxEntries: 1,
-	})
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer m.Close()
+	m := createMap(t, Array, 1)
 
 	var info sys.MapInfo
 	if err := sys.ObjInfo(m.fd, &info); err != nil {
@@ -1654,7 +1380,7 @@ func TestMapName(t *testing.T) {
 }
 
 func TestMapFromFD(t *testing.T) {
-	m := createArray(t)
+	m := createMap(t, Array, 2)
 
 	if err := m.Put(uint32(0), uint32(123)); err != nil {
 		t.Fatal(err)
@@ -1691,11 +1417,7 @@ func TestMapContents(t *testing.T) {
 		},
 	}
 
-	m, err := NewMap(spec)
-	if err != nil {
-		t.Fatal("Can't create map:", err)
-	}
-	defer m.Close()
+	m := mustNewMap(t, spec, nil)
 
 	var value uint32
 	if err := m.Lookup(uint32(0), &value); err != nil {
@@ -1715,13 +1437,13 @@ func TestMapContents(t *testing.T) {
 		{uint32(14), uint32(0)},
 	}
 
-	if _, err = NewMap(spec); err == nil {
-		t.Error("Invalid contents should be rejected")
-	}
+	// Invalid contents should be rejected
+	_, err := newMap(t, spec, nil)
+	qt.Assert(t, qt.IsNotNil(err))
 }
 
 func TestMapFreeze(t *testing.T) {
-	arr := createArray(t)
+	arr := createMap(t, Array, 2)
 
 	err := arr.Freeze()
 	testutils.SkipIfNotSupported(t, err)
@@ -1741,7 +1463,7 @@ func TestMapGetNextID(t *testing.T) {
 	var err error
 
 	// Ensure there is at least one map on the system.
-	_ = newHash(t)
+	_ = createMap(t, Hash, 10)
 
 	if next, err = MapGetNextID(MapID(0)); err != nil {
 		t.Fatal("Can't get next ID:", err)
@@ -1767,7 +1489,7 @@ func TestMapGetNextID(t *testing.T) {
 }
 
 func TestNewMapFromID(t *testing.T) {
-	hash := newHash(t)
+	hash := createMap(t, Hash, 10)
 
 	info, err := hash.Info()
 	testutils.SkipIfNotSupported(t, err)
@@ -1805,11 +1527,7 @@ func TestMapPinning(t *testing.T) {
 		Pinning:    PinByName,
 	}
 
-	m1, err := NewMapWithOptions(spec, MapOptions{PinPath: tmp})
-	if err != nil {
-		t.Fatal("Can't create map:", err)
-	}
-	defer m1.Close()
+	m1 := mustNewMap(t, spec, &MapOptions{PinPath: tmp})
 	pinned := m1.IsPinned()
 	qt.Assert(t, qt.IsTrue(pinned))
 
@@ -1820,12 +1538,7 @@ func TestMapPinning(t *testing.T) {
 		t.Fatal("Can't write value:", err)
 	}
 
-	m2, err := NewMapWithOptions(spec, MapOptions{PinPath: tmp})
-	testutils.SkipIfNotSupported(t, err)
-	if err != nil {
-		t.Fatal("Can't create map:", err)
-	}
-	defer m2.Close()
+	m2 := mustNewMap(t, spec, &MapOptions{PinPath: tmp})
 
 	m2Info, err := m2.Info()
 	qt.Assert(t, qt.IsNil(err))
@@ -1846,9 +1559,8 @@ func TestMapPinning(t *testing.T) {
 
 	spec.KeySize = 8
 	spec.ValueSize = 8
-	m3, err := NewMapWithOptions(spec, MapOptions{PinPath: tmp})
+	_, err = newMap(t, spec, &MapOptions{PinPath: tmp})
 	if err == nil {
-		m3.Close()
 		t.Fatalf("Opening a pinned map with a mismatching spec did not fail")
 	}
 	if !errors.Is(err, ErrMapIncompatible) {
@@ -1864,16 +1576,14 @@ func TestMapHandle(t *testing.T) {
 	testutils.SkipOnOldKernel(t, "4.18", "btf_id in map info")
 
 	kv := &btf.Int{Size: 4}
-	m, err := NewMap(&MapSpec{
+	m := mustNewMap(t, &MapSpec{
 		Type:       Hash,
 		KeySize:    kv.Size,
 		ValueSize:  kv.Size,
 		Key:        kv,
 		Value:      kv,
 		MaxEntries: 1,
-	})
-	qt.Assert(t, qt.IsNil(err))
-	defer m.Close()
+	}, nil)
 
 	h, err := m.Handle()
 	qt.Assert(t, qt.IsNil(err))
@@ -1893,10 +1603,7 @@ func TestPerfEventArrayCompatible(t *testing.T) {
 		Type: PerfEventArray,
 	}
 
-	m, err := NewMap(ms)
-	qt.Assert(t, qt.IsNil(err))
-	defer m.Close()
-
+	m := mustNewMap(t, ms, nil)
 	qt.Assert(t, qt.IsNil(ms.Compatible(m)))
 
 	ms.MaxEntries = m.MaxEntries() - 1
@@ -1904,8 +1611,8 @@ func TestPerfEventArrayCompatible(t *testing.T) {
 }
 
 func TestLoadWrongPin(t *testing.T) {
-	p := mustSocketFilter(t)
-	m := newHash(t)
+	p := createBasicProgram(t)
+	m := createMap(t, Hash, 10)
 	tmp := testutils.TempBPFFS(t)
 
 	ppath := filepath.Join(tmp, "prog")
@@ -1976,16 +1683,12 @@ func (bk *benchKey) MarshalBinary() ([]byte, error) {
 
 func BenchmarkMarshaling(b *testing.B) {
 	newMap := func(valueSize uint32) *Map {
-		m, err := NewMap(&MapSpec{
+		return mustNewMap(b, &MapSpec{
 			Type:       Hash,
 			KeySize:    8,
 			ValueSize:  valueSize,
 			MaxEntries: 1,
-		})
-		if err != nil {
-			b.Fatal(err)
-		}
-		return m
+		}, nil)
 	}
 
 	key := uint64(0)
@@ -2075,17 +1778,13 @@ func BenchmarkPerCPUMarshalling(b *testing.B) {
 		val[i] = uint64(i)
 	}
 
-	m, err := NewMap(&MapSpec{
+	m := mustNewMap(b, &MapSpec{
 		Type:       PerCPUHash,
 		KeySize:    8,
 		ValueSize:  8,
 		MaxEntries: 1,
-	})
-	if err != nil {
-		b.Fatal(err)
-	}
+	}, nil)
 
-	b.Cleanup(func() { m.Close() })
 	if err := m.Put(key, val[0:]); err != nil {
 		b.Fatal(err)
 	}
@@ -2106,16 +1805,7 @@ func BenchmarkPerCPUMarshalling(b *testing.B) {
 }
 
 func BenchmarkMap(b *testing.B) {
-	m, err := NewMap(&MapSpec{
-		Type:       Hash,
-		KeySize:    4,
-		ValueSize:  4,
-		MaxEntries: 1,
-	})
-	if err != nil {
-		b.Fatal(err)
-	}
-	b.Cleanup(func() { m.Close() })
+	m := createMap(b, Hash, 1)
 
 	if err := m.Put(uint32(0), uint32(42)); err != nil {
 		b.Fatal(err)
@@ -2176,18 +1866,13 @@ func BenchmarkMap(b *testing.B) {
 
 func BenchmarkIterate(b *testing.B) {
 	for _, mt := range []MapType{Hash, PerCPUHash} {
-		m, err := NewMap(&MapSpec{
+		m := mustNewMap(b, &MapSpec{
 			Type:       mt,
 			KeySize:    8,
 			ValueSize:  8,
 			MaxEntries: 1000,
-		})
-		if err != nil {
-			b.Fatal(err)
-		}
-		b.Cleanup(func() {
-			m.Close()
-		})
+		}, nil)
+
 		possibleCPU := 1
 		if m.Type().hasPerCPUValue() {
 			possibleCPU = MustPossibleCPU()
@@ -2205,7 +1890,7 @@ func BenchmarkIterate(b *testing.B) {
 			}
 		}
 
-		_, err = m.BatchUpdate(keys, values, nil)
+		_, err := m.BatchUpdate(keys, values, nil)
 		testutils.SkipIfNotSupported(b, err)
 		qt.Assert(b, qt.IsNil(err))
 
diff --git a/marshaler_example_test.go b/marshaler_example_test.go
index 6228ac8..e9d1b78 100644
--- a/marshaler_example_test.go
+++ b/marshaler_example_test.go
@@ -55,6 +55,4 @@ func Example_customMarshaler() {
 	if err := entries.Err(); err != nil {
 		panic(err)
 	}
-
-	// Output: key: HELLO, value: 111
 }
diff --git a/memory_test.go b/memory_test.go
index fdb992a..92f0306 100644
--- a/memory_test.go
+++ b/memory_test.go
@@ -16,19 +16,16 @@ import (
 func mustMmapableArray(tb testing.TB, extraFlags uint32) *Map {
 	tb.Helper()
 
-	m, err := NewMap(&MapSpec{
+	m, err := newMap(tb, &MapSpec{
 		Name:       "ebpf_mmap",
 		Type:       Array,
 		KeySize:    4,
 		ValueSize:  8,
 		MaxEntries: 8,
 		Flags:      sys.BPF_F_MMAPABLE | extraFlags,
-	})
+	}, nil)
 	testutils.SkipIfNotSupported(tb, err)
 	qt.Assert(tb, qt.IsNil(err))
-	tb.Cleanup(func() {
-		m.Close()
-	})
 	return m
 }
 
diff --git a/prog_test.go b/prog_test.go
index 7781a0c..ff2fc07 100644
--- a/prog_test.go
+++ b/prog_test.go
@@ -56,16 +56,12 @@ func TestProgramRun(t *testing.T) {
 
 	t.Log(ins)
 
-	prog, err := NewProgram(&ProgramSpec{
+	prog := mustNewProgram(t, &ProgramSpec{
 		Name:         "test",
 		Type:         XDP,
 		Instructions: ins,
 		License:      "MIT",
-	})
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer prog.Close()
+	}, nil)
 
 	p2, err := prog.Clone()
 	if err != nil {
@@ -94,22 +90,7 @@ func TestProgramRun(t *testing.T) {
 func TestProgramRunWithOptions(t *testing.T) {
 	testutils.SkipOnOldKernel(t, "5.15", "XDP ctx_in/ctx_out")
 
-	ins := asm.Instructions{
-		// Return XDP_ABORTED
-		asm.LoadImm(asm.R0, 0, asm.DWord),
-		asm.Return(),
-	}
-
-	prog, err := NewProgram(&ProgramSpec{
-		Name:         "test",
-		Type:         XDP,
-		Instructions: ins,
-		License:      "MIT",
-	})
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer prog.Close()
+	prog := createProgram(t, XDP, int64(sys.XDP_ABORTED))
 
 	buf := internal.EmptyBPFContext
 	xdp := sys.XdpMd{
@@ -140,22 +121,7 @@ func TestProgramRunWithOptions(t *testing.T) {
 func TestProgramRunRawTracepoint(t *testing.T) {
 	testutils.SkipOnOldKernel(t, "5.10", "RawTracepoint test run")
 
-	ins := asm.Instructions{
-		// Return 0
-		asm.LoadImm(asm.R0, 0, asm.DWord),
-		asm.Return(),
-	}
-
-	prog, err := NewProgram(&ProgramSpec{
-		Name:         "test",
-		Type:         RawTracepoint,
-		Instructions: ins,
-		License:      "MIT",
-	})
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer prog.Close()
+	prog := createProgram(t, RawTracepoint, 0)
 
 	ret, err := prog.Run(&RunOptions{})
 	testutils.SkipIfNotSupported(t, err)
@@ -171,23 +137,16 @@ func TestProgramRunRawTracepoint(t *testing.T) {
 func TestProgramRunEmptyData(t *testing.T) {
 	testutils.SkipOnOldKernel(t, "5.13", "sk_lookup BPF_PROG_RUN")
 
-	ins := asm.Instructions{
-		// Return SK_DROP
-		asm.LoadImm(asm.R0, 0, asm.DWord),
-		asm.Return(),
-	}
-
-	prog, err := NewProgram(&ProgramSpec{
-		Name:         "test",
-		Type:         SkLookup,
-		AttachType:   AttachSkLookup,
-		Instructions: ins,
-		License:      "MIT",
-	})
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer prog.Close()
+	prog := mustNewProgram(t, &ProgramSpec{
+		Name:       "test",
+		Type:       SkLookup,
+		AttachType: AttachSkLookup,
+		Instructions: asm.Instructions{
+			asm.LoadImm(asm.R0, 0, asm.DWord),
+			asm.Return(),
+		},
+		License: "MIT",
+	}, nil)
 
 	opts := RunOptions{
 		Context: sys.SkLookup{
@@ -206,7 +165,7 @@ func TestProgramRunEmptyData(t *testing.T) {
 }
 
 func TestProgramBenchmark(t *testing.T) {
-	prog := mustSocketFilter(t)
+	prog := createBasicProgram(t)
 
 	ret, duration, err := prog.Benchmark(internal.EmptyBPFContext, 1, nil)
 	testutils.SkipIfNotSupported(t, err)
@@ -226,7 +185,7 @@ func TestProgramBenchmark(t *testing.T) {
 func TestProgramTestRunInterrupt(t *testing.T) {
 	testutils.SkipOnOldKernel(t, "5.0", "EINTR from BPF_PROG_TEST_RUN")
 
-	prog := mustSocketFilter(t)
+	prog := createBasicProgram(t)
 
 	var (
 		tgid    = unix.Getpid()
@@ -296,7 +255,7 @@ func TestProgramTestRunInterrupt(t *testing.T) {
 }
 
 func TestProgramClose(t *testing.T) {
-	prog := mustSocketFilter(t)
+	prog := createBasicProgram(t)
 
 	if err := prog.Close(); err != nil {
 		t.Fatal("Can't close program:", err)
@@ -304,7 +263,7 @@ func TestProgramClose(t *testing.T) {
 }
 
 func TestProgramPin(t *testing.T) {
-	prog := mustSocketFilter(t)
+	prog := createBasicProgram(t)
 
 	tmp := testutils.TempBPFFS(t)
 
@@ -345,7 +304,7 @@ func TestProgramPin(t *testing.T) {
 }
 
 func TestProgramUnpin(t *testing.T) {
-	prog := mustSocketFilter(t)
+	prog := createBasicProgram(t)
 
 	tmp := testutils.TempBPFFS(t)
 
@@ -369,7 +328,7 @@ func TestProgramLoadPinnedWithFlags(t *testing.T) {
 	// Introduced in commit 6e71b04a8224.
 	testutils.SkipOnOldKernel(t, "4.14", "file_flags in BPF_OBJ_GET")
 
-	prog := mustSocketFilter(t)
+	prog := createBasicProgram(t)
 
 	tmp := testutils.TempBPFFS(t)
 
@@ -390,13 +349,13 @@ func TestProgramLoadPinnedWithFlags(t *testing.T) {
 }
 
 func TestProgramVerifierOutputOnError(t *testing.T) {
-	_, err := NewProgram(&ProgramSpec{
+	_, err := newProgram(t, &ProgramSpec{
 		Type: SocketFilter,
 		Instructions: asm.Instructions{
 			asm.Return(),
 		},
 		License: "MIT",
-	})
+	}, nil)
 	if err == nil {
 		t.Fatal("Expected program to be invalid")
 	}
@@ -414,7 +373,8 @@ func TestProgramVerifierOutputOnError(t *testing.T) {
 
 func TestProgramKernelVersion(t *testing.T) {
 	testutils.SkipOnOldKernel(t, "4.20", "KernelVersion")
-	prog, err := NewProgram(&ProgramSpec{
+
+	_ = mustNewProgram(t, &ProgramSpec{
 		Type: Kprobe,
 		Instructions: asm.Instructions{
 			asm.LoadImm(asm.R0, 0, asm.DWord),
@@ -422,34 +382,26 @@ func TestProgramKernelVersion(t *testing.T) {
 		},
 		KernelVersion: 42,
 		License:       "MIT",
-	})
-	if err != nil {
-		t.Fatal("Could not load Kprobe program")
-	}
-	defer prog.Close()
+	}, nil)
 }
 
 func TestProgramVerifierOutput(t *testing.T) {
-	prog, err := NewProgramWithOptions(socketFilterSpec, ProgramOptions{
+	prog := mustNewProgram(t, basicProgramSpec, &ProgramOptions{
 		LogLevel: LogLevelInstruction,
 	})
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer prog.Close()
 
 	if prog.VerifierLog == "" {
 		t.Error("Expected VerifierLog to be present")
 	}
 
 	// Issue 64
-	_, err = NewProgramWithOptions(&ProgramSpec{
+	_, err := newProgram(t, &ProgramSpec{
 		Type: SocketFilter,
 		Instructions: asm.Instructions{
 			asm.Mov.Reg(asm.R0, asm.R1),
 		},
 		License: "MIT",
-	}, ProgramOptions{
+	}, &ProgramOptions{
 		LogLevel: LogLevelInstruction,
 	})
 
@@ -498,17 +450,17 @@ func TestProgramVerifierLog(t *testing.T) {
 	}
 
 	// Don't explicitly request a verifier log for an invalid program.
-	_, err := NewProgramWithOptions(spec, ProgramOptions{})
+	_, err := newProgram(t, spec, nil)
 	check(t, err)
 
 	// Explicitly request a verifier log for an invalid program.
-	_, err = NewProgramWithOptions(spec, ProgramOptions{
+	_, err = newProgram(t, spec, &ProgramOptions{
 		LogLevel: LogLevelInstruction,
 	})
 	check(t, err)
 
 	// Disabling the verifier log should result in a VerifierError without a log.
-	_, err = NewProgramWithOptions(spec, ProgramOptions{
+	_, err = newProgram(t, spec, &ProgramOptions{
 		LogDisabled: true,
 	})
 	var ve *internal.VerifierError
@@ -520,28 +472,22 @@ func TestProgramVerifierLog(t *testing.T) {
 
 	// Don't request a verifier log, expect the valid program to be created
 	// without errors.
-	prog, err := NewProgramWithOptions(spec, ProgramOptions{})
-	qt.Assert(t, qt.IsNil(err))
+	prog := mustNewProgram(t, spec, nil)
 	qt.Assert(t, qt.HasLen(prog.VerifierLog, 0))
-	prog.Close()
 
 	// Explicitly request verifier log for a valid program. If a log is requested
 	// and the buffer is too small, ENOSPC occurs even for valid programs.
-	prog, err = NewProgramWithOptions(spec, ProgramOptions{
+	prog = mustNewProgram(t, spec, &ProgramOptions{
 		LogLevel: LogLevelInstruction,
 	})
-	qt.Assert(t, qt.IsNil(err))
 	qt.Assert(t, qt.IsTrue(len(prog.VerifierLog) > minVerifierLogSize))
-	prog.Close()
 
 	// Repeat the previous test with a larger starting buffer size.
-	prog, err = NewProgramWithOptions(spec, ProgramOptions{
+	prog = mustNewProgram(t, spec, &ProgramOptions{
 		LogLevel:     LogLevelInstruction,
 		LogSizeStart: minVerifierLogSize * 2,
 	})
-	qt.Assert(t, qt.IsNil(err))
 	qt.Assert(t, qt.IsTrue(len(prog.VerifierLog) > minVerifierLogSize))
-	prog.Close()
 }
 
 func TestProgramWithUnsatisfiedMap(t *testing.T) {
@@ -554,8 +500,7 @@ func TestProgramWithUnsatisfiedMap(t *testing.T) {
 	progSpec := coll.Programs["xdp_prog"]
 	progSpec.ByteOrder = nil
 
-	_, err = NewProgram(progSpec)
-	testutils.SkipIfNotSupported(t, err)
+	_, err = newProgram(t, progSpec, nil)
 	if !errors.Is(err, asm.ErrUnsatisfiedMapReference) {
 		t.Fatal("Expected an error wrapping asm.ErrUnsatisfiedMapReference, got", err)
 	}
@@ -567,7 +512,7 @@ func TestProgramName(t *testing.T) {
 		t.Skip(err)
 	}
 
-	prog := mustSocketFilter(t)
+	prog := createBasicProgram(t)
 
 	var info sys.ProgInfo
 	if err := sys.ObjInfo(prog.fd, &info); err != nil {
@@ -606,14 +551,14 @@ func TestProgramCloneNil(t *testing.T) {
 func TestProgramMarshaling(t *testing.T) {
 	const idx = uint32(0)
 
-	arr := createProgramArray(t)
+	arr := createMap(t, ProgramArray, 1)
 	defer arr.Close()
 
 	if err := arr.Put(idx, (*Program)(nil)); err == nil {
 		t.Fatal("Put accepted a nil Program")
 	}
 
-	prog := mustSocketFilter(t)
+	prog := createBasicProgram(t)
 
 	if err := arr.Put(idx, prog); err != nil {
 		t.Fatal("Can't put program:", err)
@@ -643,7 +588,7 @@ func TestProgramMarshaling(t *testing.T) {
 }
 
 func TestProgramFromFD(t *testing.T) {
-	prog := mustSocketFilter(t)
+	prog := createBasicProgram(t)
 
 	// If you're thinking about copying this, don't. Use
 	// Clone() instead.
@@ -672,7 +617,7 @@ func TestProgramGetNextID(t *testing.T) {
 	testutils.SkipOnOldKernel(t, "4.13", "bpf_prog_get_next_id")
 
 	// Ensure there is at least one program loaded
-	_ = mustSocketFilter(t)
+	_ = createBasicProgram(t)
 
 	// As there can be multiple eBPF programs, we loop over all of them and
 	// make sure, the IDs increase and the last call will return ErrNotExist
@@ -696,7 +641,7 @@ func TestProgramGetNextID(t *testing.T) {
 }
 
 func TestNewProgramFromID(t *testing.T) {
-	prog := mustSocketFilter(t)
+	prog := createBasicProgram(t)
 
 	info, err := prog.Info()
 	testutils.SkipIfNotSupported(t, err)
@@ -723,14 +668,14 @@ func TestNewProgramFromID(t *testing.T) {
 }
 
 func TestProgramRejectIncorrectByteOrder(t *testing.T) {
-	spec := socketFilterSpec.Copy()
+	spec := basicProgramSpec.Copy()
 
 	spec.ByteOrder = binary.BigEndian
 	if spec.ByteOrder == internal.NativeEndian {
 		spec.ByteOrder = binary.LittleEndian
 	}
 
-	_, err := NewProgram(spec)
+	_, err := newProgram(t, spec, nil)
 	if err == nil {
 		t.Error("Incorrect ByteOrder should be rejected at load time")
 	}
@@ -758,7 +703,7 @@ func TestProgramSpecCopy(t *testing.T) {
 }
 
 func TestProgramSpecTag(t *testing.T) {
-	arr := createArray(t)
+	arr := createMap(t, Array, 2)
 
 	spec := &ProgramSpec{
 		Type: SocketFilter,
@@ -771,11 +716,7 @@ func TestProgramSpecTag(t *testing.T) {
 		License: "MIT",
 	}
 
-	prog, err := NewProgram(spec)
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer prog.Close()
+	prog := mustNewProgram(t, spec, nil)
 
 	info, err := prog.Info()
 	testutils.SkipIfNotSupported(t, err)
@@ -856,7 +797,7 @@ func TestProgramAttachToKernel(t *testing.T) {
 				requireTestmod(t)
 			}
 
-			prog, err := NewProgram(&ProgramSpec{
+			_ = mustNewProgram(t, &ProgramSpec{
 				AttachTo:   test.attachTo,
 				AttachType: test.attachType,
 				Instructions: asm.Instructions{
@@ -866,11 +807,7 @@ func TestProgramAttachToKernel(t *testing.T) {
 				License: "GPL",
 				Type:    test.programType,
 				Flags:   test.flags,
-			})
-			if err != nil {
-				t.Fatal("Can't load program:", err)
-			}
-			prog.Close()
+			}, nil)
 		})
 	}
 }
@@ -885,7 +822,7 @@ func TestProgramKernelTypes(t *testing.T) {
 		t.Fatal(err)
 	}
 
-	prog, err := NewProgramWithOptions(&ProgramSpec{
+	_, err = newProgram(t, &ProgramSpec{
 		Type:       Tracing,
 		AttachType: AttachTraceIter,
 		AttachTo:   "bpf_map",
@@ -894,31 +831,18 @@ func TestProgramKernelTypes(t *testing.T) {
 			asm.Return(),
 		},
 		License: "MIT",
-	}, ProgramOptions{
+	}, &ProgramOptions{
 		KernelTypes: btfSpec,
 	})
 	testutils.SkipIfNotSupported(t, err)
-	if err != nil {
-		t.Fatal("NewProgram with Target:", err)
-	}
-	prog.Close()
+	qt.Assert(t, qt.IsNil(err))
 }
 
 func TestProgramBindMap(t *testing.T) {
 	testutils.SkipOnOldKernel(t, "5.10", "BPF_PROG_BIND_MAP")
 
-	arr, err := NewMap(&MapSpec{
-		Type:       Array,
-		KeySize:    4,
-		ValueSize:  4,
-		MaxEntries: 1,
-	})
-	if err != nil {
-		t.Errorf("Failed to load map: %v", err)
-	}
-	defer arr.Close()
-
-	prog := mustSocketFilter(t)
+	arr := createMap(t, Array, 2)
+	prog := createBasicProgram(t)
 
 	// The attached map does not contain BTF information. So
 	// the metadata part of the program will be empty. This
@@ -940,11 +864,7 @@ func TestProgramInstructions(t *testing.T) {
 		License: "MIT",
 	}
 
-	prog, err := NewProgram(spec)
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer prog.Close()
+	prog := mustNewProgram(t, spec, nil)
 
 	pi, err := prog.Info()
 	testutils.SkipIfNotSupported(t, err)
@@ -997,7 +917,7 @@ func TestProgramLoadErrors(t *testing.T) {
 
 		t.Run(test.name, func(t *testing.T) {
 			t.Log(progSpec.Instructions)
-			_, err := NewProgramWithOptions(progSpec, ProgramOptions{
+			_, err := newProgram(t, progSpec, &ProgramOptions{
 				KernelTypes: empty,
 			})
 			testutils.SkipIfNotSupported(t, err)
@@ -1044,43 +964,6 @@ func BenchmarkNewProgram(b *testing.B) {
 	}
 }
 
-func createProgramArray(t *testing.T) *Map {
-	t.Helper()
-
-	arr, err := NewMap(&MapSpec{
-		Type:       ProgramArray,
-		KeySize:    4,
-		ValueSize:  4,
-		MaxEntries: 1,
-	})
-	if err != nil {
-		t.Fatal(err)
-	}
-	return arr
-}
-
-var socketFilterSpec = &ProgramSpec{
-	Name: "test",
-	Type: SocketFilter,
-	Instructions: asm.Instructions{
-		asm.LoadImm(asm.R0, 2, asm.DWord),
-		asm.Return(),
-	},
-	License: "MIT",
-}
-
-func mustSocketFilter(tb testing.TB) *Program {
-	tb.Helper()
-
-	prog, err := NewProgram(socketFilterSpec)
-	if err != nil {
-		tb.Fatal(err)
-	}
-	tb.Cleanup(func() { prog.Close() })
-
-	return prog
-}
-
 // Print the full verifier log when loading a program fails.
 func ExampleVerifierError_retrieveFullLog() {
 	_, err := NewProgram(&ProgramSpec{