Agent skill
fsharp-tests
Write comprehensive tests using Expecto for F# applications. Use when: "add tests", "write tests", "test X", "unit test", "testing", "verify", "Expecto", "test coverage", "TDD", "property test", "async test", "test case". Creates tests in src/Tests/ for domain logic, validation, persistence, and state. Focus on pure functions (domain) and validation rules for best coverage.
Stars
163
Forks
31
Install this agent skill to your Project
npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/testing/fsharp-tests-heimeshoff-cinemarco-3df7f334
SKILL.md
F# Testing with Expecto
When to Use This Skill
Activate when:
- User requests "add tests for X", "test Y"
- Implementing any new feature (always write tests)
- Need to verify domain logic
- Testing validation rules
- Testing API contracts
- Testing state transitions (Elmish)
Test Project Structure
src/Tests/
├── Shared.Tests/
│ ├── DomainTests.fs
│ ├── ValidationTests.fs
│ ├── Program.fs
│ └── Shared.Tests.fsproj
│
├── Server.Tests/
│ ├── DomainTests.fs
│ ├── ValidationTests.fs
│ ├── PersistenceTests.fs
│ ├── Program.fs
│ └── Server.Tests.fsproj
│
└── Client.Tests/
├── StateTests.fs
├── Program.fs
└── Client.Tests.fsproj
Project Setup
Test Project File
xml
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<IsPackable>false</IsPackable>
<GenerateProgramFile>false</GenerateProgramFile>
</PropertyGroup>
<ItemGroup>
<Compile Include="DomainTests.fs" />
<Compile Include="ValidationTests.fs" />
<Compile Include="Program.fs" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Expecto" Version="10.2.1" />
<PackageReference Include="Expecto.FsCheck" Version="10.2.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../../Server/Server.fsproj" />
<ProjectReference Include="../../Shared/Shared.fsproj" />
</ItemGroup>
</Project>
Program.fs
fsharp
module Program
open Expecto
[<EntryPoint>]
let main args =
runTestsInAssembly defaultConfig args
Testing Domain Logic
fsharp
module DomainTests
open Expecto
open Shared.Domain
[<Tests>]
let tests =
testList "Domain Logic" [
testCase "processNewTodo trims title" <| fun () ->
let request = { Title = " Test "; Description = None; Priority = Low }
let result = Domain.processNewTodo request
Expect.equal result.Title "Test" "Should trim whitespace"
testCase "completeTodo changes status" <| fun () ->
let todo = { baseTodo with Status = Active }
let result = Domain.completeTodo todo
Expect.equal result.Status Completed "Should be completed"
testCase "completeTodo updates timestamp" <| fun () ->
let before = System.DateTime.UtcNow
let todo = { baseTodo with UpdatedAt = before }
let result = Domain.completeTodo todo
Expect.isGreaterThan result.UpdatedAt before "Should update timestamp"
testCase "calculateTotal sums correctly" <| fun () ->
let items = [
{ Item = "A"; Price = 10.0m; Quantity = 2 }
{ Item = "B"; Price = 5.0m; Quantity = 3 }
]
let total = Domain.calculateTotal items
Expect.equal total 35.0m "Should sum correctly"
]
Testing Validation
fsharp
module ValidationTests
open Expecto
open Validation
[<Tests>]
let tests =
testList "Validation" [
testCase "Valid todo passes" <| fun () ->
let todo = {
Id = 1
Title = "Valid Title"
Description = Some "Description"
Priority = Medium
Status = Active
CreatedAt = System.DateTime.UtcNow
UpdatedAt = System.DateTime.UtcNow
}
let result = validateTodoItem todo
Expect.isOk result "Should pass validation"
testCase "Empty title fails" <| fun () ->
let todo = { validTodo with Title = "" }
let result = validateTodoItem todo
Expect.isError result "Should fail validation"
testCase "Title too long fails" <| fun () ->
let todo = { validTodo with Title = String.replicate 101 "a" }
let result = validateTodoItem todo
Expect.isError result "Should fail validation"
testCase "Multiple errors accumulated" <| fun () ->
let todo = { validTodo with Title = ""; Id = -1 }
match validateTodoItem todo with
| Error errors ->
Expect.isGreaterThan errors.Length 1 "Should have multiple errors"
Expect.contains errors "Title is required" "Should mention title"
| Ok _ ->
failtest "Should have failed validation"
]
Testing Result Types
fsharp
[<Tests>]
let resultTests =
testList "Result Handling" [
testCase "Successful operation returns Ok" <| fun () ->
let result = Operation.performAction validInput
Expect.isOk result "Should succeed"
match result with
| Ok value ->
Expect.equal value.Status Success "Should be successful"
| Error _ ->
failtest "Should not fail"
testCase "Invalid input returns Error" <| fun () ->
let result = Operation.performAction invalidInput
Expect.isError result "Should fail"
match result with
| Error msg ->
Expect.stringContains msg "invalid" "Should mention invalid input"
| Ok _ ->
failtest "Should not succeed"
]
Testing Async Operations
fsharp
[<Tests>]
let asyncTests =
testList "Async Operations" [
testCaseAsync "getAllTodos returns list" <| async {
let! result = Persistence.getAllTodos()
Expect.isNotNull result "Should return list"
}
testCaseAsync "getTodoById returns todo" <| async {
let! result = Persistence.getTodoById 1
match result with
| Some todo ->
Expect.equal todo.Id 1 "Should have correct ID"
| None ->
failtest "Should find todo"
}
testCaseAsync "getTodoById returns None for nonexistent" <| async {
let! result = Persistence.getTodoById 99999
Expect.isNone result "Should not find todo"
}
]
Testing State Transitions (Elmish)
fsharp
module StateTests
open Expecto
open State
open Types
[<Tests>]
let tests =
testList "State Management" [
testCase "Init creates correct initial state" <| fun () ->
let model, cmd = State.init()
Expect.equal model.Todos NotAsked "Should start as NotAsked"
Expect.equal model.NewTodoTitle "" "Should have empty title"
testCase "LoadTodos sets Loading state" <| fun () ->
let model = { initialModel with Todos = NotAsked }
let newModel, _ = State.update LoadTodos model
Expect.equal newModel.Todos Loading "Should set to Loading"
testCase "TodosLoaded with Ok sets Success" <| fun () ->
let model = { initialModel with Todos = Loading }
let todos = [ todo1; todo2 ]
let newModel, _ = State.update (TodosLoaded (Ok todos)) model
match newModel.Todos with
| Success loadedTodos ->
Expect.equal loadedTodos todos "Should contain loaded todos"
| _ ->
failtest "Should be Success state"
testCase "TodosLoaded with Error sets Failure" <| fun () ->
let model = { initialModel with Todos = Loading }
let newModel, _ = State.update (TodosLoaded (Error "Failed")) model
match newModel.Todos with
| Failure msg ->
Expect.equal msg "Failed" "Should contain error message"
| _ ->
failtest "Should be Failure state"
testCase "UpdateNewTodoTitle updates model" <| fun () ->
let model = initialModel
let newModel, _ = State.update (UpdateNewTodoTitle "New Title") model
Expect.equal newModel.NewTodoTitle "New Title" "Should update title"
]
Property-Based Testing
fsharp
open FsCheck
[<Tests>]
let propertyTests =
testList "Property Tests" [
testProperty "Trimming is idempotent" <| fun (s: string) ->
let trimmed = s.Trim()
trimmed.Trim() = trimmed
testProperty "Adding then removing returns original count" <| fun (items: int list) (newItem: int) ->
let withItem = newItem :: items
let afterRemoval = withItem |> List.filter (fun x -> x <> newItem)
afterRemoval.Length <= items.Length + 1
]
Test Fixtures
fsharp
module TestData =
let validTodo = {
Id = 1
Title = "Test Todo"
Description = Some "Description"
Priority = Medium
Status = Active
CreatedAt = System.DateTime(2024, 1, 1)
UpdatedAt = System.DateTime(2024, 1, 1)
}
let createTodo id title =
{ validTodo with Id = id; Title = title }
let testTodos = [
createTodo 1 "First"
createTodo 2 "Second"
createTodo 3 "Third"
]
[<Tests>]
let tests =
testList "Using Test Data" [
testCase "Uses valid todo" <| fun () ->
let result = Domain.processTodo TestData.validTodo
Expect.isOk result "Should process valid todo"
]
Running Tests
bash
# Run all tests
dotnet test
# Run specific test project
dotnet test src/Tests/Server.Tests/
# Run with watch mode
dotnet test --watch
# Run with filter
dotnet test --filter "FullyQualifiedName~Validation"
# Verbose output
dotnet test --logger "console;verbosity=detailed"
Best Practices
✅ Do
- Test domain logic thoroughly (it's pure)
- Test validation rules
- Use descriptive test names
- Test edge cases and error conditions
- Keep tests independent
- Use test fixtures for common data
- Test state transitions
❌ Don't
- Test implementation details
- Make tests dependent on order
- Skip testing error cases
- Make tests dependent on external services
- Write slow tests without async
- Forget boundary conditions
Verification Checklist
- Test project created and configured
- Domain logic tests written
- Validation tests written
- Edge cases tested
- Error conditions tested
- Async operations tested
- State transitions tested (if frontend)
- All tests pass
- Tests are independent
- Descriptive test names
Related Skills
- fsharp-backend - Testing backend logic
- fsharp-frontend - Testing state management
- fsharp-validation - Testing validation
- fsharp-persistence - Testing persistence
Related Documentation
/docs/06-TESTING.md- Detailed testing guide
Didn't find tool you were looking for?