Skip to content

Commit 70f8aff

Browse files
dbrattliclaude
andcommitted
feat: add Fable.Python.Testing module for cross-platform testing
Add a Testing module that provides test utilities for writing tests that run on both .NET (with XUnit) and Python (with pytest). Features: - `equal` / `notEqual` - Assert equality/inequality (F# style: expected first) - `throwsError` - Assert exact error message - `throwsErrorContaining` - Assert error contains substring - `throwsAnyError` - Assert any error is thrown - `doesntThrow` - Assert no error is thrown - `FactAttribute` - Mark test functions for pytest discovery Uses `Fable.Core.Testing.Assert` (the standard Fable way) instead of custom Emit statements. Also removes the local test/Util.fs and updates all test files to use the new Fable.Python.Testing module directly. Release-As: 5.0.0-alpha.21.4 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
1 parent 6bcd385 commit 70f8aff

18 files changed

+212
-45
lines changed

src/Fable.Python.fsproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@
3838
<Compile Include="fastapi/FastAPI.fs" />
3939

4040
<Compile Include="fable/Types.fs" />
41+
<Compile Include="fable/Testing.fs" />
42+
</ItemGroup>
43+
<ItemGroup Condition="'$(FABLE_COMPILER)' != 'true'">
44+
<PackageReference Include="xunit" Version="2.9.*" />
4145
</ItemGroup>
4246
<ItemGroup>
4347
<Content Include="pyproject.toml; *.fsproj; **\*.fs; **\*.fsi" PackagePath="fable\" />

src/fable/Testing.fs

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/// Cross-platform test utilities for writing tests that run on both .NET (with XUnit) and Python (with pytest).
2+
///
3+
/// Example usage:
4+
/// ```fsharp
5+
/// open Fable.Python.Testing
6+
///
7+
/// [<Fact>]
8+
/// let ``test addition works`` () =
9+
/// let result = 2 + 2
10+
/// result |> equal 4
11+
///
12+
/// [<Fact>]
13+
/// let ``test throws on invalid input`` () =
14+
/// throwsAnyError (fun () -> failwith "boom")
15+
/// ```
16+
module Fable.Python.Testing
17+
18+
open System
19+
20+
#if FABLE_COMPILER
21+
open Fable.Core.Testing
22+
23+
/// Assert equality (expected first, then actual - F# style)
24+
let equal expected actual : unit = Assert.AreEqual(actual, expected)
25+
26+
/// Assert inequality
27+
let notEqual expected actual : unit = Assert.NotEqual(actual, expected)
28+
29+
/// Attribute to mark test functions (compiles to test_ prefix for pytest)
30+
type FactAttribute() =
31+
inherit System.Attribute()
32+
33+
#else
34+
open Xunit
35+
36+
/// Assert equality (expected first, then actual - F# style)
37+
let equal<'T> (expected: 'T) (actual: 'T) : unit = Assert.Equal(expected, actual)
38+
39+
/// Assert inequality
40+
let notEqual<'T> (expected: 'T) (actual: 'T) : unit = Assert.NotEqual(expected, actual)
41+
42+
/// FactAttribute is already provided by XUnit
43+
type FactAttribute = Xunit.FactAttribute
44+
#endif
45+
46+
// Exception testing helpers (work on both platforms)
47+
48+
let private run (f: unit -> 'a) =
49+
try
50+
f () |> Ok
51+
with e ->
52+
Error e.Message
53+
54+
/// Assert that a function throws an error with the exact message
55+
let throwsError (expected: string) (f: unit -> 'a) : unit =
56+
match run f with
57+
| Error actual when actual = expected -> ()
58+
| Error actual -> equal expected actual
59+
| Ok _ -> equal expected "No error was thrown"
60+
61+
/// Assert that a function throws an error containing the expected substring
62+
let throwsErrorContaining (expected: string) (f: unit -> 'a) : unit =
63+
match run f with
64+
| Error _ when String.IsNullOrEmpty expected -> ()
65+
| Error (actual: string) when actual.Contains expected -> ()
66+
| Error actual -> equal (sprintf "Error containing '%s'" expected) actual
67+
| Ok _ -> equal (sprintf "Error containing '%s'" expected) "No error was thrown"
68+
69+
/// Assert that a function throws any error
70+
let throwsAnyError (f: unit -> 'a) : unit =
71+
match run f with
72+
| Error _ -> ()
73+
| Ok _ -> equal "An error" "No error was thrown"
74+
75+
/// Assert that a function does not throw
76+
let doesntThrow (f: unit -> 'a) : unit =
77+
match run f with
78+
| Ok _ -> ()
79+
| Error msg -> equal "No error" (sprintf "Error: %s" msg)

test/Fable.Python.Test.fsproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
<ProjectReference Include="../src/Fable.Python.fsproj" />
1313
</ItemGroup>
1414
<ItemGroup>
15-
<Compile Include="Util.fs" />
1615
<Compile Include="TestAst.fs" />
1716
<Compile Include="TestAsyncIO.fs" />
1817
<Compile Include="TestBuiltins.fs" />
@@ -26,6 +25,7 @@
2625
<Compile Include="TestPydantic.fs" />
2726
<Compile Include="TestFastAPI.fs" />
2827
<Compile Include="TestTypes.fs" />
28+
<Compile Include="TestTesting.fs" />
2929
<Compile Include="Main.fs" />
3030
</ItemGroup>
3131
<Import Project="..\.paket\Paket.Restore.targets" />

test/TestAst.fs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
module Fable.Python.Tests.Ast
22

3-
open Util.Testing
3+
open Fable.Python.Testing
44
open Fable.Python.Ast
55

66
[<Fact>]

test/TestAsyncIO.fs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
module Fable.Python.Tests.AsyncIO
22

3-
open Util.Testing
3+
open Fable.Python.Testing
44
open Fable.Python.AsyncIO
55

66
[<Fact>]

test/TestBase64.fs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
module Fable.Python.Tests.Base64
22

3-
open Util.Testing
3+
open Fable.Python.Testing
44
open Fable.Python.Base64
55
open Fable.Python.Builtins
66

test/TestBuiltins.fs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
module Fable.Python.Tests.Builtins
22

3-
open Util.Testing
3+
open Fable.Python.Testing
44
open Fable.Python.Builtins
55
open Fable.Python.Os
66

test/TestFastAPI.fs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
module Fable.Python.Tests.TestFastAPI
22

3-
open Fable.Python.Tests.Util.Testing
3+
open Fable.Python.Testing
44

55
#if FABLE_COMPILER
66
open Fable.Core

test/TestJson.fs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
module Fable.Python.Tests.Json
22

3-
open Util.Testing
3+
open Fable.Python.Testing
44
open Fable.Python.Json
55

66
// Test types for union and record serialization

test/TestLogging.fs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
module Fable.Python.Tests.Logging
22

3-
open Util.Testing
3+
open Fable.Python.Testing
44
open Fable.Python.Logging
55

66
[<Fact>]

0 commit comments

Comments
 (0)