ArchUnitGo

Write the sentence your team already says out loud — the api does not touch the database — as a value, and let go test tell you where the code disagrees.

func TestTheApiDoesNotTouchTheDatabase(t *testing.T) {
	rule := archunit.ProjectFiles(nil).
		InFolder("internal/api/**").
		ShouldNot().
		DependOnFiles().
		InFolder("internal/db/**")

	archunit.AssertPasses(t, rule, nil)
}

That is the whole setup: no fixture, no registration, no configuration file. A rule is a value, nil means the defaults everywhere, and the only thing that reads your project is the terminal at the end of the chain.

Install

go get github.com/LukasNiessen/ArchUnitGo

Go 1.26 or newer. The only direct dependency is golang.org/x/tools, which is how the extractor talks to the Go toolchain. There is no tagged release yet, so go get resolves to a pseudo-version of main and the API may still move.

The package is archunit while the last element of the module path is ArchUnitGo, so give the import the name it has:

import archunit "github.com/LukasNiessen/ArchUnitGo"

Your first rule

An architecture rule is a normal test in a normal package. Put it wherever your other tests live:

package architecture_test

import (
	"testing"

	archunit "github.com/LukasNiessen/ArchUnitGo"
)

func TestNoFileDependsOnAnotherInACircle(t *testing.T) {
	archunit.AssertPasses(t, archunit.ProjectFiles(nil).Should().HaveNoCycles(), nil)
}

Three things are worth knowing before you write the second one:

What a failure looks like

One t.Error, carrying the rule as it was written and then the violations, numbered from one:

project files, path without filename matches "internal/api/**", should not, depend on files, path without filename matches "internal/db/**"
1 violation:
  1. internal/api/handler.go: should not, depend on files, path without filename matches "internal/db/**"; it depends on internal/db/conn.go

The rule’s own sentence comes first, so a test asserting several rules says which one broke before it says what broke it. A selector renders as the part of an identifier it was matched against rather than as the verb that spelled it — InFolder reads back as path without filename matches — because that is what you have to compare your glob against. See patterns and identifiers for what those parts are.

Where to go next

Two things this site deliberately does not hold. The API reference is pkg.go.dev, generated from the doc comments in the source, because that is where a Go reader looks for one and a second copy would be a second thing to go stale. The list of what is not implemented yet is in the README, which is the one place it is stated and tested.