diff --git a/src/Fable.Python.fsproj b/src/Fable.Python.fsproj
index 42effae..1d38c5f 100644
--- a/src/Fable.Python.fsproj
+++ b/src/Fable.Python.fsproj
@@ -38,6 +38,10 @@
+
+
+
+
diff --git a/src/fable/Testing.fs b/src/fable/Testing.fs
new file mode 100644
index 0000000..f1b9193
--- /dev/null
+++ b/src/fable/Testing.fs
@@ -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
+///
+/// []
+/// let ``test addition works`` () =
+/// let result = 2 + 2
+/// result |> equal 4
+///
+/// []
+/// 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)
diff --git a/test/Fable.Python.Test.fsproj b/test/Fable.Python.Test.fsproj
index 6292242..539890b 100644
--- a/test/Fable.Python.Test.fsproj
+++ b/test/Fable.Python.Test.fsproj
@@ -12,7 +12,6 @@
-
@@ -26,6 +25,7 @@
+
diff --git a/test/TestAst.fs b/test/TestAst.fs
index 3cfde42..c733535 100644
--- a/test/TestAst.fs
+++ b/test/TestAst.fs
@@ -1,6 +1,6 @@
module Fable.Python.Tests.Ast
-open Util.Testing
+open Fable.Python.Testing
open Fable.Python.Ast
[]
diff --git a/test/TestAsyncIO.fs b/test/TestAsyncIO.fs
index 0f73981..1be8cb5 100644
--- a/test/TestAsyncIO.fs
+++ b/test/TestAsyncIO.fs
@@ -1,6 +1,6 @@
module Fable.Python.Tests.AsyncIO
-open Util.Testing
+open Fable.Python.Testing
open Fable.Python.AsyncIO
[]
diff --git a/test/TestBase64.fs b/test/TestBase64.fs
index 05e0686..3d18999 100644
--- a/test/TestBase64.fs
+++ b/test/TestBase64.fs
@@ -1,6 +1,6 @@
module Fable.Python.Tests.Base64
-open Util.Testing
+open Fable.Python.Testing
open Fable.Python.Base64
open Fable.Python.Builtins
diff --git a/test/TestBuiltins.fs b/test/TestBuiltins.fs
index 19ea2df..5aee957 100644
--- a/test/TestBuiltins.fs
+++ b/test/TestBuiltins.fs
@@ -1,6 +1,6 @@
module Fable.Python.Tests.Builtins
-open Util.Testing
+open Fable.Python.Testing
open Fable.Python.Builtins
open Fable.Python.Os
diff --git a/test/TestFastAPI.fs b/test/TestFastAPI.fs
index 73818d9..4ec54d8 100644
--- a/test/TestFastAPI.fs
+++ b/test/TestFastAPI.fs
@@ -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
diff --git a/test/TestJson.fs b/test/TestJson.fs
index b4e18af..1ba7543 100644
--- a/test/TestJson.fs
+++ b/test/TestJson.fs
@@ -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
diff --git a/test/TestLogging.fs b/test/TestLogging.fs
index 8f95323..939c0c8 100644
--- a/test/TestLogging.fs
+++ b/test/TestLogging.fs
@@ -1,6 +1,6 @@
module Fable.Python.Tests.Logging
-open Util.Testing
+open Fable.Python.Testing
open Fable.Python.Logging
[]
diff --git a/test/TestMath.fs b/test/TestMath.fs
index 55349f8..759fc8f 100644
--- a/test/TestMath.fs
+++ b/test/TestMath.fs
@@ -1,6 +1,6 @@
module Fable.Python.Tests.Math
-open Util.Testing
+open Fable.Python.Testing
open Fable.Python.Math
[]
diff --git a/test/TestOs.fs b/test/TestOs.fs
index f01ab88..07a1488 100644
--- a/test/TestOs.fs
+++ b/test/TestOs.fs
@@ -1,6 +1,6 @@
module Fable.Python.Tests.Os
-open Util.Testing
+open Fable.Python.Testing
open Fable.Python.Os
[]
diff --git a/test/TestPydantic.fs b/test/TestPydantic.fs
index a3f9ed0..81039c3 100644
--- a/test/TestPydantic.fs
+++ b/test/TestPydantic.fs
@@ -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
diff --git a/test/TestRandom.fs b/test/TestRandom.fs
index 7dfbae0..ca3e470 100644
--- a/test/TestRandom.fs
+++ b/test/TestRandom.fs
@@ -1,6 +1,6 @@
module Fable.Python.Tests.Random
-open Util.Testing
+open Fable.Python.Testing
open Fable.Python.Random
[]
diff --git a/test/TestString.fs b/test/TestString.fs
index d449e77..5d1b808 100644
--- a/test/TestString.fs
+++ b/test/TestString.fs
@@ -1,6 +1,6 @@
module Fable.Python.Tests.String
-open Util.Testing
+open Fable.Python.Testing
open Fable.Python.String
[]
diff --git a/test/TestTesting.fs b/test/TestTesting.fs
new file mode 100644
index 0000000..cead90a
--- /dev/null
+++ b/test/TestTesting.fs
@@ -0,0 +1,115 @@
+module Fable.Python.Tests.Testing
+
+open Fable.Python.Testing
+
+// ============================================================================
+// Test that equal works correctly
+// ============================================================================
+
+[]
+let ``test equal passes for equal values`` () =
+ equal 1 1
+ equal "hello" "hello"
+ equal true true
+ equal [1; 2; 3] [1; 2; 3]
+
+[]
+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)
+
+[]
+let ``test equal fails for unequal strings`` () =
+ throwsAnyError (fun () -> equal "hello" "world")
+
+[]
+let ``test equal fails for unequal booleans`` () =
+ throwsAnyError (fun () -> equal true false)
+
+// ============================================================================
+// Test that notEqual works correctly
+// ============================================================================
+
+[]
+let ``test notEqual passes for unequal values`` () =
+ notEqual 1 2
+ notEqual "hello" "world"
+ notEqual true false
+
+[]
+let ``test notEqual fails for equal values`` () =
+ throwsAnyError (fun () -> notEqual 1 1)
+
+[]
+let ``test notEqual fails for equal strings`` () =
+ throwsAnyError (fun () -> notEqual "hello" "hello")
+
+// ============================================================================
+// Test throwsAnyError
+// ============================================================================
+
+[]
+let ``test throwsAnyError passes when function throws`` () =
+ throwsAnyError (fun () -> failwith "boom")
+
+[]
+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
+// ============================================================================
+
+[]
+let ``test doesntThrow passes when function succeeds`` () =
+ doesntThrow (fun () -> 1 + 1)
+
+[]
+let ``test doesntThrow fails when function throws`` () =
+ throwsAnyError (fun () ->
+ doesntThrow (fun () -> failwith "boom")
+ )
+
+// ============================================================================
+// Test throwsError with exact message
+// ============================================================================
+
+[]
+let ``test throwsError passes with matching message`` () =
+ throwsError "exact error" (fun () -> failwith "exact error")
+
+[]
+let ``test throwsError fails with wrong message`` () =
+ throwsAnyError (fun () ->
+ throwsError "expected message" (fun () -> failwith "different message")
+ )
+
+[]
+let ``test throwsError fails when no error thrown`` () =
+ throwsAnyError (fun () ->
+ throwsError "expected error" (fun () -> 42)
+ )
+
+// ============================================================================
+// Test throwsErrorContaining
+// ============================================================================
+
+[]
+let ``test throwsErrorContaining passes when message contains substring`` () =
+ throwsErrorContaining "partial" (fun () -> failwith "this is a partial match error")
+
+[]
+let ``test throwsErrorContaining fails when message does not contain substring`` () =
+ throwsAnyError (fun () ->
+ throwsErrorContaining "notfound" (fun () -> failwith "different error message")
+ )
+
+[]
+let ``test throwsErrorContaining fails when no error thrown`` () =
+ throwsAnyError (fun () ->
+ throwsErrorContaining "error" (fun () -> 42)
+ )
diff --git a/test/TestTypes.fs b/test/TestTypes.fs
index 66743c8..41a4df4 100644
--- a/test/TestTypes.fs
+++ b/test/TestTypes.fs
@@ -1,6 +1,6 @@
module Fable.Python.Tests.Types
-open Util.Testing
+open Fable.Python.Testing
open Fable.Python.Fable.Types
// Test typeName function
diff --git a/test/Util.fs b/test/Util.fs
deleted file mode 100644
index d765e47..0000000
--- a/test/Util.fs
+++ /dev/null
@@ -1,31 +0,0 @@
-module Fable.Python.Tests.Util
-
-module Testing =
-#if FABLE_COMPILER
- open Fable.Core
- open Fable.Core.PyInterop
-
- type Assert =
- []
- static member AreEqual(actual: 'T, expected: 'T, ?msg: string) : unit = nativeOnly
-
- []
- static member NotEqual(actual: 'T, expected: 'T, ?msg: string) : unit = nativeOnly
-
- let equal expected actual : unit = Assert.AreEqual(actual, expected)
- let notEqual expected actual : unit = Assert.NotEqual(actual, expected)
-
- type Fact() =
- inherit System.Attribute()
-#else
- open Xunit
- type FactAttribute = Xunit.FactAttribute
-
- let equal<'T> (expected: 'T) (actual: 'T) : unit = Assert.Equal(expected, actual)
- let notEqual<'T> (expected: 'T) (actual: 'T) : unit = Assert.NotEqual(expected, actual)
-#endif
- let rec sumFirstSeq (zs: seq) (n: int) : float =
- match n with
- | 0 -> 0.
- | 1 -> Seq.head zs
- | _ -> (Seq.head zs) + sumFirstSeq (Seq.skip 1 zs) (n - 1)