Get started on your computer
In this tutorial we’ll install Varar into a project, configure it, run a first oath, and then deliberately break it. By the end you will have seen an oath pass and fail as a test on your own machine.
Pick your preferred programming language in the language selector in the top-right corner of the page. Every command and code sample below follows that choice.
Before you start
Section titled “Before you start”You need your language’s toolchain already installed — Varar adds a library and a test-framework adapter to a project you can already build and test.
Node.js 22.18 or newer. Node strips TypeScript types by itself from 22.18 on. Older versions fail the run with 'Unknown file extension ".ts"', and need NODE_OPTIONS=--experimental-strip-types.
JDK 21 or newer, with Maven or Gradle. The published artifacts target Java 21.
JDK 21 or newer, with Gradle and Kotlin 2.x. The published artifacts target Java 21.
Python 3.11 or newer. The commands below use uv; pip and poetry work the same way.
Ruby 3.2 or newer, with Bundler.
Rust 1.85 or newer, installed with rustup. The crates are edition 2024, which 1.85 is the first release to support.
The .NET 10 SDK or newer. The packages target net10.0.
Go 1.26 or newer.
-
Install
Terminal window pnpm add -D @varar/varar @varar/cli<!-- pom.xml — use the latest release from Maven Central --><dependency><groupId>dev.varar</groupId><artifactId>junit</artifactId><version>0.8.0</version><scope>test</scope></dependency>// build.gradle.kts — use the latest release from Maven CentraltestImplementation("dev.varar:kotlin:0.8.0")testImplementation("dev.varar:junit:0.8.0")Terminal window uv add --dev pytest-vararTerminal window bundle add varar-rspecTerminal window cargo add varar-cargotest --devTerminal window dotnet add package Varar.TestAdapterTerminal window go get github.com/varar-dev/varar/goThis adds Varar’s author API (the package your step definitions import) and the adapter that runs oaths in your test framework.
-
Configure
Varar is configured by one file,
varar.config.json— the single source of truth for which Markdown files are oaths and which files bind their steps. Ports with avararCLI scaffold it (and a working example) in one command; on the others you create it by hand.Terminal window pnpm exec varar initOne command writes the config and a working example oath:
created varar.config.jsoncreated varar/deep-thought.mdcreated src/varar/deep-thought.steps.tsupdated package.json (added "type": "module")varar.config.jsondecides which Markdown files are oaths and which files bind their steps:varar.config.json {"docs": { "include": ["varar/**/*.md"], "exclude": [] },"steps": ["src/varar/**/*.steps.ts"]}The Java port has no
varar inityet, so configuration is two small files you create by hand.varar.config.jsondecides which Markdown files are oaths — and lists step classes by package wildcard instead of a file glob:varar.*means every step class in thevararpackage:varar.config.json {"docs": { "include": ["varar/**/*.md"], "exclude": [] },"steps": ["varar.*"]}A one-line JUnit suite makes Maven or Gradle discover the oaths:
src/test/java/varar/RunVararTest.java package varar;import org.junit.platform.suite.api.IncludeEngines;import org.junit.platform.suite.api.SelectDirectories;import org.junit.platform.suite.api.Suite;@Suite @IncludeEngines("varar") @SelectDirectories(".") public class RunVararTest {}The Maven and Gradle sample projects are complete, runnable copies you can start from.
The Kotlin port has no
varar inityet, so configuration is two small files you create by hand.varar.config.jsondecides which Markdown files are oaths — and lists step classes by package wildcard instead of a file glob:varar.*means every step class in thevararpackage:varar.config.json {"docs": { "include": ["varar/**/*.md"], "exclude": [] },"steps": ["varar.*"]}A one-line JUnit suite makes Maven or Gradle discover the oaths:
src/test/kotlin/varar/RunVararTest.kt package vararimport org.junit.platform.suite.api.IncludeEnginesimport org.junit.platform.suite.api.SelectDirectoriesimport org.junit.platform.suite.api.Suite@Suite @IncludeEngines("varar") @SelectDirectories(".") class RunVararTestThe Kotlin sample project is a complete, runnable copy you can start from.
Terminal window uv run varar initOne command writes the config and a working example oath:
created varar.config.jsoncreated varar/deep-thought.mdcreated tests/varar/deep_thought.steps.pyvarar.config.jsondecides which Markdown files are oaths and which files bind their steps:varar.config.json {"docs": { "include": ["varar/**/*.md"], "exclude": [] },"steps": ["tests/varar/**/*.steps.py"]}Terminal window bundle exec varar initOne command writes the config and a working example oath:
created varar.config.jsoncreated varar/deep-thought.mdcreated spec/varar/deep_thought.steps.rbcreated spec/varar_spec.rb(With
varar-minitestinstalled instead ofvarar-rspec, the bridge lands intest/varar_test.rband the steps intest/varar/.)varar.config.jsondecides which Markdown files are oaths and which files bind their steps:varar.config.json {"docs": { "include": ["varar/**/*.md"], "exclude": [] },"steps": ["spec/varar/**/*.steps.rb"]}The Rust port has no
varar inityet, so createvarar.config.jsonat the project root by hand. It decides which Markdown files are oaths and which files bind their steps:varar.config.json {"docs": { "include": ["varar/**/*.md"], "exclude": [] },"steps": ["src/varar/*.steps.rs"]}A small
tests/varar.rsharness hands the oaths tocargo test— copy it from the Rust sample project — a complete, runnable copy you can start from.The C# port has no
varar inityet, so createvarar.config.jsonat the project root by hand. It decides which Markdown files are oaths and which files bind their steps:varar.config.json {"docs": { "include": ["varar/**/*.md"], "exclude": [] },"steps": ["steps/*.steps.cs"]}dotnet testdiscovers the oaths through theVarar.TestAdapterpackage — copy the project wiring from the C# sample project — a complete, runnable copy you can start from.The Go port has no
varar inityet, so createvarar.config.jsonat the project root by hand. It decides which Markdown files are oaths and which files bind their steps:varar.config.json {"docs": { "include": ["varar/**/*.md"], "exclude": [] },"steps": ["*.steps.go"]}A small
varar_test.goharness hands the oaths togo test— copy it from the Go sample project — a complete, runnable copy you can start from. -
A first oath
The example oath is plain Markdown with one concrete example —
varar initalready wrote it for you; if your port has no CLI, create it now, in thevarar/directory your config’sdocsglobs match:varar/deep-thought.md # Deep ThoughtYou're really not going to like it.The answer to the great question of life, the universe and everything is 42.It was a tough assignment.Notice there are no keywords and no special syntax — it’s ordinary Markdown prose. One sentence makes a checkable claim; Varar matches that phrase, and everything around it is just documentation for the reader.
The steps file binds that sentence to code. A sensor reads the software and returns what it actually produced, for Varar to compare against the number in the Markdown:
src/varar/deep-thought.steps.ts import { steps } from '@varar/varar'const { sensor } = steps()sensor('life, the universe and everything is {int}', () => 42)src/test/java/varar/DeepThoughtSteps.java package varar;import dev.varar.State;import dev.varar.StepDefinitions;import dev.varar.Steps;public final class DeepThoughtSteps implements StepDefinitions<DeepThoughtSteps.Ctx> {record Ctx() implements State {}@Overridepublic void register(Steps<Ctx> s) {s.state(Ctx::new);s.sensor("life, the universe and everything is {int}", (Ctx ctx, Integer answer) -> 42);}}src/test/kotlin/varar/deep-thought.steps.kt @file:JvmName("DeepThoughtSteps")package vararimport dev.varar.kotlin.sensorimport dev.varar.kotlin.stepsval deepThoughtSteps =steps {sensor("life, the universe and everything is {int}") { _: Int -> 42 }}tests/varar/deep_thought.steps.py from varar import stepsparam, stimulus, sensor = steps()@sensor("life, the universe and everything is {int}")def _(state, answer):return 42spec/varar/deep_thought.steps.rb require 'varar'steps dosensor('life, the universe and everything is {int}') { 42 }endsrc/varar/deep_thought.steps.rs use varar::Steps;pub fn register(s: &mut Steps<()>) {s.sensor("life, the universe and everything is {int}", |_ctx: (), _answer: i64| Ok(42));}steps/deep-thought.steps.cs using Varar;using Varar.Core;namespace Examples;public static class DeepThoughtSteps{public static void Register(Steps s){s.Sensor("life, the universe and everything is {int}", (state, answer) => Value.Of(42));}}deep_thought.steps.go package examplesimport "github.com/varar-dev/varar/go/varar"func Register(s *varar.Steps) {s.Sensor("life, the universe and everything is {int}", func(state varar.Value, args []varar.Value) (*varar.Value, error) {return varar.Ptr(varar.IntValue(42)), nil})} -
Run it
Terminal window pnpm vitest runTerminal window mvn testTerminal window ./gradlew testTerminal window uv run pytestTerminal window bundle exec rspecTerminal window cargo testTerminal window dotnet testTerminal window go testVarar reports one example passing:
varar/deep-thought.md✓ Deep Thought1 example, 1 passed, 0 failedpytest, RSpec, JUnit, cargo test, dotnet test and go test each report the same example passing in their own format. The Markdown file just ran as a test, and the software kept its word.
-
Watch it fail on purpose
Never trust a test you haven’t seen fail. Open the steps file and change the answer the sensor returns from
42to43:sensor('life, the universe and everything is {int}', () => 43)s.sensor("life, the universe and everything is {int}", (Ctx ctx, Integer answer) -> 43);sensor("life, the universe and everything is {int}") { _: Int -> 43 }@sensor("life, the universe and everything is {int}")def _(state, answer):return 43sensor('life, the universe and everything is {int}') { 43 }s.sensor("life, the universe and everything is {int}",file!(),line!() as usize,Handler::sync1(|_state, _answer| Ok(Some(Value::Int(43)))),);s.Sensor("life, the universe and everything is {int}", (state, answer) => Value.Of(43));s.Sensor("life, the universe and everything is {int}", func(state varar.Value, args []varar.Value) (*varar.Value, error) {return varar.Ptr(varar.IntValue(43)), nil})Run Varar again. The example now fails: the oath still says
42, but the sensor observed43. The output shows both values and points at the exact42in the Markdown where the promise broke.Revert the change and run once more — one example, one passed. You have seen both sides of the contract.
- Your first oath from scratch — write your own oath in a blank Markdown file and bind its steps.
- Already have a vitest suite? Run oaths through vitest instead of the CLI.