Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/Fable.Python.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@
<Compile Include="fastapi/FastAPI.fs" />

<Compile Include="fable/Types.fs" />
<Compile Include="fable/Testing.fs" />
</ItemGroup>
<ItemGroup Condition="'$(FABLE_COMPILER)' != 'true'">
<PackageReference Include="xunit" Version="2.9.*" />
</ItemGroup>
<ItemGroup>
<Content Include="pyproject.toml; *.fsproj; **\*.fs; **\*.fsi" PackagePath="fable\" />
Expand Down
79 changes: 79 additions & 0 deletions src/fable/Testing.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/// Cross-platform test utilities for writing tests that run on both .NET (with XUnit) and Python (with pytest).
///
/// Example usage:
/// ```fsharp
/// open Fable.Python.Testing
///
/// [<Fact>]
/// let ``test addition works`` () =
/// let result = 2 + 2
/// result |> equal 4
///
/// [<Fact>]
/// let ``test throws on invalid input`` () =
/// throwsAnyError (fun () -> failwith "boom")
/// ```
module Fable.Python.Testing

open System

#if FABLE_COMPILER
open Fable.Core.Testing

/// Assert equality (expected first, then actual - F# style)
let equal expected actual : unit = Assert.AreEqual(actual, expected)

/// Assert inequality
let notEqual expected actual : unit = Assert.NotEqual(actual, expected)

/// Attribute to mark test functions (compiles to test_ prefix for pytest)
type FactAttribute() =
inherit System.Attribute()

#else
open Xunit

/// Assert equality (expected first, then actual - F# style)
let equal<'T> (expected: 'T) (actual: 'T) : unit = Assert.Equal(expected, actual)

/// Assert inequality
let notEqual<'T> (expected: 'T) (actual: 'T) : unit = Assert.NotEqual(expected, actual)

/// FactAttribute is already provided by XUnit
type FactAttribute = Xunit.FactAttribute
#endif

// Exception testing helpers (work on both platforms)

let private run (f: unit -> 'a) =
try
f () |> Ok
with e ->
Error e.Message

/// Assert that a function throws an error with the exact message
let throwsError (expected: string) (f: unit -> 'a) : unit =
match run f with
| Error actual when actual = expected -> ()
| Error actual -> equal expected actual
| Ok _ -> equal expected "No error was thrown"

/// Assert that a function throws an error containing the expected substring
let throwsErrorContaining (expected: string) (f: unit -> 'a) : unit =
match run f with
| Error _ when String.IsNullOrEmpty expected -> ()
| Error (actual: string) when actual.Contains expected -> ()
| Error actual -> equal (sprintf "Error containing '%s'" expected) actual
| Ok _ -> equal (sprintf "Error containing '%s'" expected) "No error was thrown"

/// Assert that a function throws any error
let throwsAnyError (f: unit -> 'a) : unit =
match run f with
| Error _ -> ()
| Ok _ -> equal "An error" "No error was thrown"

/// Assert that a function does not throw
let doesntThrow (f: unit -> 'a) : unit =
match run f with
| Ok _ -> ()
| Error msg -> equal "No error" (sprintf "Error: %s" msg)
2 changes: 1 addition & 1 deletion test/Fable.Python.Test.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
<ProjectReference Include="../src/Fable.Python.fsproj" />
</ItemGroup>
<ItemGroup>
<Compile Include="Util.fs" />
<Compile Include="TestAst.fs" />
<Compile Include="TestAsyncIO.fs" />
<Compile Include="TestBuiltins.fs" />
Expand All @@ -26,6 +25,7 @@
<Compile Include="TestPydantic.fs" />
<Compile Include="TestFastAPI.fs" />
<Compile Include="TestTypes.fs" />
<Compile Include="TestTesting.fs" />
<Compile Include="Main.fs" />
</ItemGroup>
<Import Project="..\.paket\Paket.Restore.targets" />
Expand Down
2 changes: 1 addition & 1 deletion test/TestAst.fs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module Fable.Python.Tests.Ast

open Util.Testing
open Fable.Python.Testing
open Fable.Python.Ast

[<Fact>]
Expand Down
2 changes: 1 addition & 1 deletion test/TestAsyncIO.fs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module Fable.Python.Tests.AsyncIO

open Util.Testing
open Fable.Python.Testing
open Fable.Python.AsyncIO

[<Fact>]
Expand Down
2 changes: 1 addition & 1 deletion test/TestBase64.fs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module Fable.Python.Tests.Base64

open Util.Testing
open Fable.Python.Testing
open Fable.Python.Base64
open Fable.Python.Builtins

Expand Down
2 changes: 1 addition & 1 deletion test/TestBuiltins.fs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module Fable.Python.Tests.Builtins

open Util.Testing
open Fable.Python.Testing
open Fable.Python.Builtins
open Fable.Python.Os

Expand Down
2 changes: 1 addition & 1 deletion test/TestFastAPI.fs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module Fable.Python.Tests.TestFastAPI

open Fable.Python.Tests.Util.Testing
open Fable.Python.Testing

#if FABLE_COMPILER
open Fable.Core
Expand Down
2 changes: 1 addition & 1 deletion test/TestJson.fs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module Fable.Python.Tests.Json

open Util.Testing
open Fable.Python.Testing
open Fable.Python.Json

// Test types for union and record serialization
Expand Down
2 changes: 1 addition & 1 deletion test/TestLogging.fs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module Fable.Python.Tests.Logging

open Util.Testing
open Fable.Python.Testing
open Fable.Python.Logging

[<Fact>]
Expand Down
2 changes: 1 addition & 1 deletion test/TestMath.fs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module Fable.Python.Tests.Math

open Util.Testing
open Fable.Python.Testing
open Fable.Python.Math

[<Fact>]
Expand Down
2 changes: 1 addition & 1 deletion test/TestOs.fs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module Fable.Python.Tests.Os

open Util.Testing
open Fable.Python.Testing
open Fable.Python.Os

[<Fact>]
Expand Down
2 changes: 1 addition & 1 deletion test/TestPydantic.fs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module Fable.Python.Tests.TestPydantic

open Fable.Python.Tests.Util.Testing
open Fable.Python.Testing

#if FABLE_COMPILER
open Fable.Core
Expand Down
2 changes: 1 addition & 1 deletion test/TestRandom.fs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module Fable.Python.Tests.Random

open Util.Testing
open Fable.Python.Testing
open Fable.Python.Random

[<Fact>]
Expand Down
2 changes: 1 addition & 1 deletion test/TestString.fs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module Fable.Python.Tests.String

open Util.Testing
open Fable.Python.Testing
open Fable.Python.String

[<Fact>]
Expand Down
115 changes: 115 additions & 0 deletions test/TestTesting.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
module Fable.Python.Tests.Testing

open Fable.Python.Testing

// ============================================================================
// Test that equal works correctly
// ============================================================================

[<Fact>]
let ``test equal passes for equal values`` () =
equal 1 1
equal "hello" "hello"
equal true true
equal [1; 2; 3] [1; 2; 3]

[<Fact>]
let ``test equal fails for unequal values`` () =
// This tests that equal actually fails when values differ
// We use throwsAnyError to verify the assertion throws
throwsAnyError (fun () -> equal 1 2)

[<Fact>]
let ``test equal fails for unequal strings`` () =
throwsAnyError (fun () -> equal "hello" "world")

[<Fact>]
let ``test equal fails for unequal booleans`` () =
throwsAnyError (fun () -> equal true false)

// ============================================================================
// Test that notEqual works correctly
// ============================================================================

[<Fact>]
let ``test notEqual passes for unequal values`` () =
notEqual 1 2
notEqual "hello" "world"
notEqual true false

[<Fact>]
let ``test notEqual fails for equal values`` () =
throwsAnyError (fun () -> notEqual 1 1)

[<Fact>]
let ``test notEqual fails for equal strings`` () =
throwsAnyError (fun () -> notEqual "hello" "hello")

// ============================================================================
// Test throwsAnyError
// ============================================================================

[<Fact>]
let ``test throwsAnyError passes when function throws`` () =
throwsAnyError (fun () -> failwith "boom")

[<Fact>]
let ``test throwsAnyError fails when function does not throw`` () =
// Meta-test: throwsAnyError should fail if the function doesn't throw
throwsAnyError (fun () ->
throwsAnyError (fun () -> 42)
)

// ============================================================================
// Test doesntThrow
// ============================================================================

[<Fact>]
let ``test doesntThrow passes when function succeeds`` () =
doesntThrow (fun () -> 1 + 1)

[<Fact>]
let ``test doesntThrow fails when function throws`` () =
throwsAnyError (fun () ->
doesntThrow (fun () -> failwith "boom")
)

// ============================================================================
// Test throwsError with exact message
// ============================================================================

[<Fact>]
let ``test throwsError passes with matching message`` () =
throwsError "exact error" (fun () -> failwith "exact error")

[<Fact>]
let ``test throwsError fails with wrong message`` () =
throwsAnyError (fun () ->
throwsError "expected message" (fun () -> failwith "different message")
)

[<Fact>]
let ``test throwsError fails when no error thrown`` () =
throwsAnyError (fun () ->
throwsError "expected error" (fun () -> 42)
)

// ============================================================================
// Test throwsErrorContaining
// ============================================================================

[<Fact>]
let ``test throwsErrorContaining passes when message contains substring`` () =
throwsErrorContaining "partial" (fun () -> failwith "this is a partial match error")

[<Fact>]
let ``test throwsErrorContaining fails when message does not contain substring`` () =
throwsAnyError (fun () ->
throwsErrorContaining "notfound" (fun () -> failwith "different error message")
)

[<Fact>]
let ``test throwsErrorContaining fails when no error thrown`` () =
throwsAnyError (fun () ->
throwsErrorContaining "error" (fun () -> 42)
)
2 changes: 1 addition & 1 deletion test/TestTypes.fs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module Fable.Python.Tests.Types

open Util.Testing
open Fable.Python.Testing
open Fable.Python.Fable.Types

// Test typeName function
Expand Down
31 changes: 0 additions & 31 deletions test/Util.fs

This file was deleted.