{"id":17197655,"url":"https://github.com/dmcg/minutest","last_synced_at":"2025-04-13T19:31:07.056Z","repository":{"id":66701962,"uuid":"148457652","full_name":"dmcg/minutest","owner":"dmcg","description":"Simple, Expressive, Extensible Testing for Kotlin on the JVM","archived":false,"fork":false,"pushed_at":"2023-05-27T21:33:46.000Z","size":2063,"stargazers_count":105,"open_issues_count":4,"forks_count":11,"subscribers_count":8,"default_branch":"master","last_synced_at":"2025-04-05T07:27:13.673Z","etag":null,"topics":["kotlin","kotlin-library","testing"],"latest_commit_sha":null,"homepage":"","language":"Kotlin","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/dmcg.png","metadata":{"files":{"readme":"docs/README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2018-09-12T09:44:46.000Z","updated_at":"2025-03-16T13:46:33.000Z","dependencies_parsed_at":null,"dependency_job_id":"d1f679a9-c2fe-4ed3-9bc6-f854fd08693a","html_url":"https://github.com/dmcg/minutest","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dmcg%2Fminutest","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dmcg%2Fminutest/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dmcg%2Fminutest/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dmcg%2Fminutest/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/dmcg","download_url":"https://codeload.github.com/dmcg/minutest/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248767861,"owners_count":21158546,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["kotlin","kotlin-library","testing"],"created_at":"2024-10-15T01:57:00.199Z","updated_at":"2025-04-13T19:31:06.595Z","avatar_url":"https://github.com/dmcg.png","language":"Kotlin","funding_links":[],"categories":[],"sub_categories":[],"readme":"[![Download](https://api.bintray.com/packages/dmcg/oneeyedmen-mvn/minutest.dev/images/download.svg)](https://bintray.com/dmcg/oneeyedmen-mvn/minutest.dev/_latestVersion)\n[![Build Status](https://travis-ci.org/dmcg/minutest.svg?branch=master)](https://travis-ci.org/dmcg/minutest)\n\n# Minutest\n\nJUnit multiplied by Kotlin\n\n## Why Another Test Framework?\n\nJUnit is great for quickly writing and running tests as part of a TDD workflow, but try to do anything unusual and you have to reach for the documentation and specially written annotations. \n\nMinutest extends JUnit with a simple model that allows you to solve your own problems with plain Kotlin.\n\nFor example\n\n### Conditionally running a test\n\nJUnit has a special annotation\n\n```kotlin\n@Test\n@EnabledIfEnvironmentVariable(named = \"ENV\", matches = \"staging-server\")\nfun onlyOnStagingServer() {\n    // ...\n}\n```\n\nMinutest is just Kotlin\n\n```kotlin\nif (getenv(\"ENV\") == \"staging-server\" ) test(\"only on staging server\") {\n    // ...\n}\n```\n\n### Parameterised tests\n\nJUnit has three annotations\n\n```kotlin\n@DisplayName(\"Fruit tests\")\n@ParameterizedTest(name = \"{index} ==\u003e fruit=''{0}'', rank={1}\")\n@CsvSource(\"apple, 1\", \"banana, 2\", \"'lemon, lime', 3\")\nfun testWithCustomDisplayNames(fruit: String, rank, String) {\n    // ...\n}\n```\n\nMinutest is just Kotlin\n\n```kotlin\ncontext(\"Fruit tests\") {\n    listOf(\"apple\" to 1, \"banana\" to 2, \"lemon, lime\" to 3).forEachIndexed { index, (fruit, rank) -\u003e\n        test(\"$index ==\u003e fruit='$fruit', rank=$rank\") {\n            // ...\n        }\n    }\n}\n```\n\n### Nested Tests\n\nJUnit needs more annotations\n\n[start-insert]: \u003c../core/src/test/kotlin/dev/minutest/examples/StackExampleTestsJUnit.kt\u003e\n```kotlin\n@DisplayName(\"A stack\")\nclass TestingAStackDemo {\n\n    var stack: Stack\u003cAny\u003e = Stack()\n\n    @Nested\n    @DisplayName(\"when new\")\n    inner class WhenNew {\n\n        @Test\n        fun `is empty`() {\n            assertTrue(stack.isEmpty())\n        }\n    }\n\n    @Nested\n    @DisplayName(\"after pushing an element\")\n    inner class AfterPushing {\n\n        var anElement = \"an element\"\n\n        @BeforeEach\n        fun pushAnElement() {\n            stack.push(anElement)\n        }\n\n        @Test\n        fun `it is no longer empty`() {\n            assertFalse(stack.isEmpty())\n        }\n    }\n}\n```\n\u003csmall\u003e[../core/src/test/kotlin/dev/minutest/examples/StackExampleTestsJUnit.kt](../core/src/test/kotlin/dev/minutest/examples/StackExampleTestsJUnit.kt)\u003c/small\u003e\n\n[end-insert]: \u003c\u003e\n\nMinutest is just Kotlin\n\n[start-insert]: \u003c../core/src/test/kotlin/dev/minutest/examples/StackExampleTests.kt\u003e\n```kotlin\nclass StackExampleTests : JUnit5Minutests {\n\n    fun tests() = rootContext\u003cStack\u003cAny\u003e\u003e {\n\n        given { Stack() }\n\n        context(\"when new\") {\n\n            test(\"is empty\") {\n                assertTrue(it.isEmpty())\n            }\n        }\n\n        context(\"after pushing an element\") {\n\n            beforeEach {\n                it.push(\"an element\")\n            }\n\n            test(\"it is no longer empty\") {\n                assertFalse(it.isEmpty())\n            }\n        }\n    }\n}\n```\n\u003csmall\u003e[../core/src/test/kotlin/dev/minutest/examples/StackExampleTests.kt](../core/src/test/kotlin/dev/minutest/examples/StackExampleTests.kt)\u003c/small\u003e\n\n[end-insert]: \u003c\u003e\n\n\nMinutest brings the power of Kotlin to JUnit, providing\n\n* A clean DSL to define nested contexts and tests\n* Generation and manipulation of tests at runtime \n* Much easier reuse of test code\n\nFor more information on how why Minutest is like it is, see [My New Test Model](http://oneeyedmen.com/my-new-test-model.html) .\n\n## Installation\n\n[Instructions](installation.md)\n\n## Moving from JUnit to Minutest\n\nHere is a version of the JUnit 5 [first test case](https://junit.org/junit5/docs/current/user-guide/#writing-tests), converted to Kotlin.\n\n[start-insert]: \u003c../core/src/test/kotlin/dev/minutest/examples/MyFirstJUnitJupiterTests.kt\u003e\n```kotlin\nclass MyFirstJUnitJupiterTests {\n\n    private val calculator = Calculator()\n\n    @Test\n    fun addition() {\n        calculator.add(2)\n        assertEquals(2, calculator.currentValue)\n    }\n\n    @Test\n    fun subtraction() {\n        calculator.subtract(2)\n        assertEquals(-2, calculator.currentValue)\n    }\n}\n```\n\u003csmall\u003e[../core/src/test/kotlin/dev/minutest/examples/MyFirstJUnitJupiterTests.kt](../core/src/test/kotlin/dev/minutest/examples/MyFirstJUnitJupiterTests.kt)\u003c/small\u003e\n\n[end-insert]: \u003c\u003e\n\nIn Minutest it looks like this\n\n[start-insert]: \u003c../core/src/test/kotlin/dev/minutest/examples/MyFirstMinutests.kt\u003e\n```kotlin\n// Mix-in JUnit5Minutests to run Minutests with JUnit 5\n//\n// (JUnit 4 support is also available, see [JUnit4Minutests].)\nclass MyFirstMinutests : JUnit5Minutests {\n\n    // tests are grouped in a context\n    fun tests() = rootContext\u003cCalculator\u003e {\n\n        // We need to tell Minutest how to build the fixture\n        given { Calculator() }\n\n        // define a test with a test block\n        test(\"addition\") {\n            // inside tests, the fixture is `it`\n            it.add(2)\n            assertEquals(2, it.currentValue)\n        }\n\n        // each new test gets its own new fixture\n        test(\"subtraction\") { calculator -\u003e\n            subtract(2)\n            assertEquals(-2, calculator.currentValue)\n        }\n    }\n}\n```\n\u003csmall\u003e[../core/src/test/kotlin/dev/minutest/examples/MyFirstMinutests.kt](../core/src/test/kotlin/dev/minutest/examples/MyFirstMinutests.kt)\u003c/small\u003e\n\n[end-insert]: \u003c\u003e\n\nMost tests require access to some state. The collection of state required by the tests is called the test fixture. In JUnit we use the fields of the test class as the fixture - in this case just the calculator. JUnit uses a fresh instance of the test class for each test method run, which is why the state of calculator after `addition` does not affect the result of `subtraction`.\n\nMinutest does not create a fresh instance of the test class for each test, instead it invokes a `fixture` block in a context and passes the result into tests as `this`.\n\nTests for cooperating components will typically have more state than just the thing we are testing. In this case make the fixture hold all the state. \n\n[start-insert]: \u003c../core/src/test/kotlin/dev/minutest/examples/fixtures/CompoundFixtureExampleTests.kt\u003e\n```kotlin\nclass ControlPanel(\n    private val beep: () -\u003e Unit,\n    private val launchRocket: () -\u003e Unit\n) {\n    private var keyTurned: Boolean = false\n\n    fun turnKey() {\n        keyTurned = true\n    }\n\n    fun pressButton() {\n        if (keyTurned)\n            launchRocket()\n        else\n            beep()\n    }\n    val warningLightOn get() = keyTurned\n}\n\nclass CompoundFixtureExampleTests : JUnit5Minutests {\n\n    // The fixture consists of all the state affected by tests\n    class Fixture {\n        var beeped = false\n        var launched = false\n\n        val controlPanel = ControlPanel(\n            beep = { beeped = true },\n            launchRocket = { launched = true }\n        )\n    }\n\n    fun tests() = rootContext\u003cFixture\u003e {\n        given { Fixture() }\n\n        context(\"key not turned\") {\n            test(\"light is off\") {\n                assertFalse(controlPanel.warningLightOn)\n            }\n            test(\"cannot launch when pressing button\") {\n                controlPanel.pressButton()\n                assertTrue(beeped)\n                assertFalse(launched)\n            }\n        }\n\n        context(\"key turned\") {\n            beforeEach {\n                controlPanel.turnKey()\n            }\n            test(\"light is on\") {\n                assertTrue(controlPanel.warningLightOn)\n            }\n            test(\"launches when pressing button\") {\n                controlPanel.pressButton()\n                assertFalse(beeped)\n                assertTrue(launched)\n            }\n        }\n    }\n}\n```\n\u003csmall\u003e[../core/src/test/kotlin/dev/minutest/examples/fixtures/CompoundFixtureExampleTests.kt](../core/src/test/kotlin/dev/minutest/examples/fixtures/CompoundFixtureExampleTests.kt)\u003c/small\u003e\n\n[end-insert]: \u003c\u003e\n\nUnderstanding fixtures is key to Minutest - [read more](fixtures.md)\n\n## Run Code to Make Tests\n\nThe key to Minutest is that by separating the fixture from the test code, both are made available to manipulate as data. \n\nFor example, parameterised tests require [special handling](https://junit.org/junit5/docs/current/user-guide/#writing-tests-parameterized-tests) in JUnit, but not in Minutest.\n\n[start-insert]: \u003c../core/src/test/kotlin/dev/minutest/examples/ParameterisedExampleTests.kt\u003e\n```kotlin\nclass ParameterisedExampleTests : JUnit5Minutests {\n\n    fun tests() = rootContext {\n\n        context(\"palindromes\") {\n\n            // Creating a test for each of multiple parameters is as easy as\n            // calling `test()` for each one.\n            listOf(\"a\", \"oo\", \"racecar\", \"able was I ere I saw elba\").forEach { candidate -\u003e\n                test(\"$candidate is a palindrome\") {\n                    assertTrue(candidate.isPalindrome())\n                }\n            }\n        }\n        context(\"not palindromes\") {\n            listOf(\"\", \"ab\", \"a man a plan a canal pananma\").forEach { candidate -\u003e\n                test(\"$candidate is not a palindrome\") {\n                    assertFalse(candidate.isPalindrome())\n                }\n            }\n        }\n\n        // Minutest will check that the following tests are run\n        willRun(\n            \"▾ tests\",\n            \"  ▾ palindromes\",\n            \"    ✓ a is a palindrome\",\n            \"    ✓ oo is a palindrome\",\n            \"    ✓ racecar is a palindrome\",\n            \"    ✓ able was I ere I saw elba is a palindrome\",\n            \"  ▾ not palindromes\",\n            \"    ✓  is not a palindrome\",\n            \"    ✓ ab is not a palindrome\",\n            \"    ✓ a man a plan a canal pananma is not a palindrome\"\n        )\n    }\n}\n\nfun String.isPalindrome(): Boolean =\n    if (length == 0) false\n    else (0 until length / 2).find { index -\u003e this[index] != this[length - index - 1] } == null\n```\n\u003csmall\u003e[../core/src/test/kotlin/dev/minutest/examples/ParameterisedExampleTests.kt](../core/src/test/kotlin/dev/minutest/examples/ParameterisedExampleTests.kt)\u003c/small\u003e\n\n[end-insert]: \u003c\u003e\n\n## Reuse Tests\n\nMore complicated scenarios can be approached by writing your own function that returns a test or a context.\n \nIf you want to reuse the same tests for different concrete implementations, define a context with a function and call it for subclasses. Some people call this a contract.\n\n[start-insert]: \u003c../core/src/test/kotlin/dev/minutest/examples/ContractsExampleTests.kt\u003e\n```kotlin\n// To run the same tests against different implementations, first define a ContextBuilder extension function\n// that defines the tests you want run.\nfun ContextBuilder\u003cMutableCollection\u003cString\u003e\u003e.behavesAsMutableCollection() {\n\n    context(\"behaves as MutableCollection\") {\n\n        test(\"is empty when created\") {\n            assertTrue(isEmpty())\n        }\n\n        test(\"can add\") {\n            add(\"item\")\n            assertEquals(\"item\", first())\n        }\n    }\n}\n\n// Now tests can supply the fixture and invoke the function to create the tests to verify the contract.\nclass ArrayListTests : JUnit5Minutests {\n\n    fun tests() = rootContext\u003cMutableCollection\u003cString\u003e\u003e {\n        given {\n            ArrayList()\n        }\n\n        behavesAsMutableCollection()\n    }\n}\n\n// We can reuse the contract for different collections.\nclass LinkedListTests : JUnit5Minutests {\n\n    fun tests() = rootContext\u003cMutableCollection\u003cString\u003e\u003e {\n        given {\n            LinkedList()\n        }\n\n        behavesAsMutableCollection()\n    }\n}\n```\n\u003csmall\u003e[../core/src/test/kotlin/dev/minutest/examples/ContractsExampleTests.kt](../core/src/test/kotlin/dev/minutest/examples/ContractsExampleTests.kt)\u003c/small\u003e\n\n[end-insert]: \u003c\u003e\n\n\n## Structure Tests\n\nWhen your tests grow so that they need more structure, Minutest has extensions to support Given When Then blocks\n \n[start-insert]: \u003c../core/src/test/kotlin/dev/minutest/examples/scenarios/ScenariosExampleTests.kt\u003e\n```kotlin\nclass ControlPanel(\n    private val beep: () -\u003e Unit,\n    private val launchRocket: () -\u003e Unit\n) {\n    private var keyTurned: Boolean = false\n\n    fun turnKey() {\n        keyTurned = true\n    }\n\n    fun pressButton(): Boolean =\n        when {\n            keyTurned -\u003e {\n                launchRocket()\n                true\n            }\n            else -\u003e {\n                beep()\n                false\n            }\n        }\n\n    val warningLightOn get() = keyTurned\n}\n\nclass ScenariosExampleTests : JUnit5Minutests {\n\n    class Fixture {\n        var beeped = false\n        var launched = false\n\n        val controlPanel = ControlPanel(\n            beep = { beeped = true },\n            launchRocket = { launched = true }\n        )\n    }\n\n    fun tests() = rootContext\u003cFixture\u003e {\n\n        // Scenario defines a nested context\n        Scenario(\"Cannot launch without key switch\") {\n\n            // GivenFixture sets up the fixture for the scenario\n            GivenFixture(\"key not turned\") {\n                Fixture()\n            }.Then(\"warning light is off\") {\n                // Then can check the setup\n                assertFalse(controlPanel.warningLightOn)\n            }\n\n            // When performs the operation\n            When(\"pressing the button\") {\n                controlPanel.pressButton()\n            }.Then(\"result was false\") { result -\u003e\n                // Then has access to the result\n                assertFalse(result)\n            }.And(\"it beeped\") {\n                // And makes another Thens with checks\n                assertTrue(beeped)\n            }.And(\"rocket was not launched\") {\n                assertFalse(launched)\n            }\n        }\n\n        Scenario(\"Will launch with key switch\") {\n            GivenFixture(\"key turned\") {\n                Fixture().apply {\n                    controlPanel.turnKey()\n                }\n            }.Then(\"warning light is on\") {\n                assertTrue(controlPanel.warningLightOn)\n            }\n\n            When(\"pressing the button\") {\n                controlPanel.pressButton()\n            }.Then(\"result was true\") { result -\u003e\n                assertTrue(result)\n            }.And(\"it didn't beep\") {\n                assertFalse(beeped)\n            }.And(\"rocket was launched\") {\n                assertTrue(launched)\n            }\n        }\n    }\n}\n```\n\u003csmall\u003e[../core/src/test/kotlin/dev/minutest/examples/scenarios/ScenariosExampleTests.kt](../core/src/test/kotlin/dev/minutest/examples/scenarios/ScenariosExampleTests.kt)\u003c/small\u003e\n\n[end-insert]: \u003c\u003e\n\n## Other Features\n\nThe [Cookbook](Cookbook.md) shows other ways to use Minutest. \n\n## Evolution\n\nWe're pretty happy with the core Minutest language and expect not to make any breaking changes without a major version update. Features like JUnit 4 support and test annotations are public but experimental - if you use anything in an `experimental` package you should expect it to change between minor releases, and move completely once adopted into the stable core.\n\nNote that we aim for source and not binary compatibility. Some implementation may move from  methods to extension functions, or from constructors to top level or companion-object functions.\n\n## Support\n\nThe best bet for feedback and help is the [#minutest channel on the Kotlin Slack](https://kotlinlang.slack.com/messages/CCYE00YM6). See you there.","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdmcg%2Fminutest","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdmcg%2Fminutest","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdmcg%2Fminutest/lists"}