Skip to content

Commit 6c5eae9

Browse files
Sample C# code for Model-base testing (#702)
* C# code for Counter * Type and method names are formatted as code * C# code for model-base test specification
1 parent a026b4f commit 6c5eae9

File tree

2 files changed

+152
-5
lines changed

2 files changed

+152
-5
lines changed

docs/StatefulTestingNew.fsx

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,15 @@ type Counter(?initial:int) =
2525
member __.Reset() = n <- 0
2626
override __.ToString() = sprintf "Counter = %i" n
2727
28-
(**
29-
As a model to test this class we can use an int value which is an abstraction of the object's internal state. The
30-
idea is that each operation on the class (in this case, Inc, Dec and Reset) affects both the model object and the actual object, and
28+
(** In C#:
29+
30+
[lang=csharp,file=../examples/CSharp.DocSnippets/StatefulTesting.cs,key=Counter]
31+
32+
As a model to test this class we can use an `int` value which is an abstraction of the object's internal state. The
33+
idea is that each operation on the class (in this case, `Inc`, `Dec` and `Reset`) affects both the model object and the actual object, and
3134
after each such operation, the model and the actual instance should still be equivalent.
3235
33-
With this idea in mind, you can write a specification of the Counter class using an int model as follows:*)
36+
With this idea in mind, you can write a specification of the `Counter` class using an int model as follows:*)
3437
3538
let spec =
3639
let inc =
@@ -60,6 +63,12 @@ let spec =
6063
member __.Next _ = Gen.elements [ inc; dec ] }
6164
6265
(**
66+
67+
In C#:
68+
69+
[lang=csharp,file=../examples/CSharp.DocSnippets/StatefulTesting.cs,key=Specification]
70+
71+
6372
Let's break this down a bit. A specification is put together as an object that is a subtype of the abstract class `Machine<'TypeUnderTest,'ModelType>`.
6473
What you're actually defining is a state machine which can simultaneously apply operations, or state transitions, to the actual system
6574
under test (in this case, a simple object) and a model of the system under test.
@@ -88,7 +97,7 @@ calls `Inc()` or `Dec()` on the `Counter` instance, and checks that after that t
8897
silly, for demonstration purposes) precondition that the value should be strictly greater than 0 - if we run this machine our `Counter` throws if we call `Dec`
8998
so when we run this specification we can check that FsCheck does the right thing here (testing the test library, very meta...)
9099
91-
We also override ToString in each `Operation` so that counter-examples can be printed.
100+
We also override `ToString` in each `Operation` so that counter-examples can be printed.
92101
93102
A specification can be checked as follows:*)
94103
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
namespace CSharp.DocSnippets;
2+
3+
using System;
4+
using System.Collections.Generic;
5+
using FsCheck;
6+
using FsCheck.Experimental;
7+
using FsCheck.Fluent;
8+
using Microsoft.FSharp.Collections;
9+
using Xunit;
10+
11+
public class StatefulTesting
12+
{
13+
//[Counter]
14+
public class Counter
15+
{
16+
internal int N { get; private set; }
17+
18+
internal Counter(int n)
19+
{
20+
N = n;
21+
}
22+
23+
internal void Inc()
24+
{
25+
if (N <= 3)
26+
N += 1;
27+
else
28+
N = -N + 2;
29+
}
30+
31+
internal void Dec()
32+
{
33+
if (N <= 0)
34+
throw new InvalidOperationException("Precondition fail");
35+
36+
N -= 1;
37+
}
38+
39+
internal void Reset()
40+
{
41+
N = 0;
42+
}
43+
44+
public override string ToString() => $"Counter = {N}";
45+
}
46+
//[/Counter]
47+
48+
//[Specification]
49+
private class CounterSpec
50+
{
51+
internal class Inc : Operation<Counter, int>
52+
{
53+
public override bool Pre(int m)
54+
{
55+
return m > 0;
56+
}
57+
58+
public override int Run(int m)
59+
{
60+
return m + 1;
61+
}
62+
63+
public override Property Check(Counter c, int m)
64+
{
65+
return (m == c.N).Label($"Inc: model = {m}, actual = {c.N}");
66+
}
67+
68+
public override string ToString() => "inc";
69+
}
70+
71+
internal class Dec : Operation<Counter, int>
72+
{
73+
public override bool Pre(int m)
74+
{
75+
return m > 0;
76+
}
77+
78+
public override int Run(int m)
79+
{
80+
return m - 1;
81+
}
82+
83+
84+
public override Property Check(Counter c, int m)
85+
{
86+
c.Dec();
87+
return (m == c.N).Label($"Dec: model = {m}, actual = {c.N}");
88+
}
89+
90+
public override string ToString() => "dec";
91+
}
92+
93+
internal class CounterSetup : Setup<Counter, int>
94+
{
95+
private readonly int _initialValue;
96+
97+
internal CounterSetup(int initialValue)
98+
{
99+
_initialValue = initialValue;
100+
}
101+
102+
public override Counter Actual() => new(_initialValue);
103+
104+
public override int Model() => _initialValue;
105+
}
106+
}
107+
108+
public class Specification : Machine<Counter, int>
109+
{
110+
public Specification(int maxNumberOfCommands) : base(maxNumberOfCommands) { }
111+
112+
public override Arbitrary<Setup<Counter, int>> Setup =>
113+
Arb.From(Gen.Choose(0, 3)
114+
.Select<int, Setup<Counter, int>>(i => new CounterSpec.CounterSetup(i)));
115+
116+
public override TearDown<Counter> TearDown => new DisposeCall<Counter>();
117+
118+
public override Gen<Operation<Counter, int>> Next(int _) =>
119+
Gen.Elements(new Operation<Counter, int>[]
120+
{
121+
new CounterSpec.Inc(),
122+
new CounterSpec.Dec()
123+
});
124+
125+
public override IEnumerable<FSharpList<Operation<Counter, int>>> ShrinkOperations(FSharpList<Operation<Counter, int>> operation) =>
126+
base.ShrinkOperations(operation);
127+
}
128+
//[/Specification]
129+
130+
131+
[Fact]
132+
void counter_tested_with_stateful_testing()
133+
{
134+
var property = new Specification(10).ToProperty();
135+
136+
Check.QuickThrowOnFailure(property);
137+
}
138+
}

0 commit comments

Comments
 (0)