Liking cljdoc? Tell your friends :D

Test Filter: Intelligent Test Selection for Clojure

Overview

Test Filter is an intelligent test selection tool for Clojure projects that dramatically reduces test execution time by running only tests affected by your code changes. Instead of running your entire test suite on every change, Test Filter analyzes your source code dependencies and git history to determine the minimum set of tests that need to run.

This project source and documentation was completely written by Claude Code. It wrote more than I wanted and I’ve not had time to verify the documentation. Feel free to play with it, but I make ZERO guarantees that anything works as advertised.

Key Features

  • Dependency-aware: Builds a complete symbol dependency graph using static analysis

  • Git-integrated: Tracks changes between revisions to identify affected code

  • Intelligent caching: Incrementally updates analysis, avoiding full re-scans

  • Multiple test frameworks: Supports clojure.test, fulcro-spec, and custom test macros

  • Integration test handling: Special handling for integration tests with broad dependencies

  • CLJC support: Properly handles Clojure Common files with reader conditionals

  • Flexible output: Multiple formats for integration with various test runners

How It Works

  1. Build source graph: Analyze your codebase with clj-kondo to create a dependency graph

  2. Cache with git revision: Store the graph along with the current git commit SHA

  3. Detect changes: Use git diff to identify modified code since the cached revision

  4. Walk dependencies: Traverse the graph backwards from tests to find transitive dependencies

  5. Select tests: Run only tests whose dependencies include changed code

Example Flow
Source Change: modify function `foo/bar` at line 42
↓
Consult Graph: which symbols use `foo/bar`?
↓
Transitive Walk: `baz/qux` uses `foo/bar`
                 `app/handler` uses `baz/qux`
                 `app-test/handler-test` tests `app/handler`
↓
Test Selection: run `app-test/handler-test`
                (and any other tests that transitively depend on `foo/bar`)

Installation

Add to your deps.edn:

{:deps {test-filter/test-filter {:git/url "https://github.com/yourusername/test-filter"
                                  :sha "..."}}

 :aliases
 {:cli {:main-opts ["-m" "test-filter.cli"]}}}

Usage

Command Line Interface

Analyze Command

Build or update the dependency graph cache:

clojure -M:cli analyze

This command:

  • Runs clj-kondo analysis on your source code

  • Builds a complete symbol dependency graph

  • Stores it in .test-filter-cache.edn with the current git revision

Select Command

Find tests affected by changes:

# Basic usage
clojure -M:cli select

# With verbose output showing statistics
clojure -M:cli select -v

# Force re-analysis (ignore cache)
clojure -M:cli select --force

# Get all tests (ignore changes)
clojure -M:cli select --all

Output Formats

# Fully-qualified test vars (default)
clojure -M:cli select -o vars

# Test namespaces only
clojure -M:cli select -o namespaces

# Kaocha command-line format
clojure -M:cli select -o kaocha

Status Command

Check cache status:

clojure -M:cli status

Shows:

  • Whether cache exists

  • Cached git revision

  • Current git revision

  • Number of symbols and dependencies

  • Cache age

Clear Command

Invalidate the cache:

clojure -M:cli clear

Programmatic API

Use Test Filter from the REPL or your code:

(require '[test-filter.core :as core])

;; Analyze the codebase and build cache
(core/analyze!)

;; Select tests based on changes
(def result (core/select-tests :verbose true))

;; Show affected tests
(core/print-tests (:tests result) :format :namespaces)

;; Check statistics
(:stats result)
;; => {:total-symbols 153
;;     :total-dependencies 355
;;     :total-tests 12
;;     :affected-tests 3
;;     :tests-skipped 9}

Integration with Test Runners

Kaocha

# Run only affected tests with Kaocha
clojure -M:cli select -o kaocha | xargs clojure -M:kaocha

Cognitect test-runner

# Get affected test namespaces
TESTS=$(clojure -M:cli select -o namespaces)

# Run with test-runner
if [ -n "$TESTS" ]; then
  clojure -M:test -n $TESTS
fi

Typical Workflow

# 1. Initial analysis (run once or after major changes)
clojure -M:cli analyze

# 2. Make code changes
# ... edit files ...

# 3. Commit changes
git add .
git commit -m "Added feature X"

# 4. Select and view affected tests
clojure -M:cli select -v

# 5. Run only affected tests
clojure -M:cli select -o kaocha | xargs clojure -M:kaocha

CI/CD Integration

#!/bin/bash
# In your CI pipeline

# Cache the analysis from main branch
git checkout main
clojure -M:cli analyze

# Checkout PR branch
git checkout $PR_BRANCH

# Select and run affected tests
TESTS=$(clojure -M:cli select -o namespaces)
if [ -n "$TESTS" ]; then
  echo "Running affected tests: $TESTS"
  clojure -M:kaocha --focus $TESTS
else
  echo "No tests affected by changes"
fi

Advanced Features

Macro-Based Test Detection

Test Filter supports test frameworks that use macros instead of deftest:

(ns my-app.spec-test
  (:require [fulcro-spec.core :refer [specification assertions]]))

(specification "User registration"
  (assertions
    "creates a new user"
    (register-user {:name "Alice"}) => {:id 1 :name "Alice"}))

Detected test frameworks:

  • fulcro-spec.core/specification

  • Custom macros (configurable)

Integration Test Handling

Integration tests often have broad dependencies. Test Filter detects them by namespace pattern (.integration.) and applies special handling:

(ns my-app.integration.api-test
  (:require [clojure.test :refer [deftest is]]
            [my-app.system :as system]))

(deftest test-user-api
  (let [sys (system/start)]
    ;; Integration test
    (is (= 200 (:status (api-call sys))))))

Options for integration tests:

  1. Conservative mode (default): Run integration tests when uncertain about dependencies

  2. Metadata targeting: Specify exact dependencies with :test-targets metadata

  3. Configuration file: External configuration for complex cases

CLJC File Support

Test Filter properly handles Clojure Common (.cljc) files with reader conditionals:

(ns my-app.utils
  #?(:clj (:import [java.nio.file Paths])))

(defn normalize-path [path]
  #?(:clj  (-> (Paths/get path (into-array String []))
               (.normalize)
               (.toString))
     :cljs (.normalize js/path path)))
  • Analyzes only the :clj side of CLJC files

  • Ignores pure .cljs files

  • Tracks dependencies correctly across platforms

Architecture

Components

ComponentDescription

Analyzer (analyzer.clj)

Uses clj-kondo to extract var definitions, namespace definitions, and usage relationships

Graph (graph.clj)

Builds directed dependency graph using Loom library; provides traversal operations

Git (git.clj)

Wraps git commands to detect changes between revisions; parses unified diff format

Cache (cache.clj)

Persists graph to EDN format; handles incremental updates and cache invalidation

Core (core.clj)

Main test selection algorithm; coordinates all components

CLI (cli.clj)

Command-line interface with multiple output formats

Data Model

Symbol Node

{:symbol 'my.ns/foo
 :type :var
 :file "src/my/ns.clj"
 :line 42
 :end-line 47
 :defined-by 'defn
 :metadata {:private false
            :macro false
            :test? false}}

Dependency Edge

{:from 'my.ns/foo
 :to 'other.ns/bar
 :context 'my.ns/foo}

Cache Structure

{:revision "abc123def456"
 :analyzed-at "2025-01-09T10:30:00Z"
 :nodes {symbol -> node-data}
 :edges [{:from :to :context}]
 :files {"src/my/ns.clj" {:symbols [...]
                          :revision "abc123"}}}

Project Status

✅ Completed Features

All planned phases (1-9) are complete:

  • Foundation and project setup

  • clj-kondo integration

  • Graph operations with Loom

  • Git integration and change detection

  • Cache persistence and incremental updates

  • Test selection algorithm

  • Command-line interface

  • Real-world testing and bug fixes

  • Macro-based test detection (fulcro-spec)

  • Integration test handling

  • CLJC file support

Known Limitations

  1. Testing scope: Needs validation on larger codebases (>100k LOC)

  2. Dynamic requires: Conservative handling (assumes dependency)

  3. Circular dependencies: Not yet optimized

  4. ClojureScript: Not supported (by design, focuses on CLJ/CLJC)

Future Enhancements

  • Support for test.check generative tests

  • Parallel test execution planning

  • Coverage-based refinement

  • Watch mode for continuous testing

  • Configuration file for custom patterns

Contributing

Contributions are welcome! Please:

  1. Fork the repository

  2. Create a feature branch

  3. Add tests for new functionality

  4. Ensure all tests pass

  5. Submit a pull request

License

MIT License

Copyright (c) 2025

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

Support

For issues, questions, or suggestions:

  • Open an issue on GitHub

  • Check existing documentation in PLAN.md and STATUS.md

  • Review code examples in namespace docstrings

Acknowledgments

Built with:

  • clj-kondo - Static analysis

  • Loom - Graph algorithms

  • Clojure - The language that makes this possible

Can you improve this documentation?Edit on GitHub

cljdoc builds & hosts documentation for Clojure/Script libraries

Keyboard shortcuts
Ctrl+kJump to recent docs
Move to previous article
Move to next article
Ctrl+/Jump to the search field
× close