Skip to content

Commit

Permalink
Update documentation for 0.2.0-alpha
Browse files Browse the repository at this point in the history
  • Loading branch information
eredmo committed May 11, 2019
1 parent 59919a4 commit 4fc9524
Show file tree
Hide file tree
Showing 7 changed files with 95 additions and 59 deletions.
4 changes: 2 additions & 2 deletions source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@
author = u'Esbjörn Redmo'

# The short X.Y version
version = '0.1.12'
version = '0.2.0'
# The full version, including alpha/beta/rc tags
release = '0.1.12-alpha'
release = '0.2.0-alpha'


# -- General configuration ---------------------------------------------------
Expand Down
19 changes: 13 additions & 6 deletions source/extensions/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,13 @@ See for instance the following interface / mock implementation pair:
[MocklisClass]
public class MockSample : ISample
{
// The contents of this class were created by the Mocklis code-generator.
// Any changes you make will be overwritten if the contents are re-generated.

public MockSample()
{
MonthlyPayment = new FuncMethodMock<(double loanSize, double interestRate, int numberOfMonths), double>(this, "MockSample", "ISample", "MonthlyPayment", "MonthlyPayment");
Parse = new FuncMethodMock<string, (bool returnValue, int value)>(this, "MockSample", "ISample", "Parse", "Parse");
MonthlyPayment = new FuncMethodMock<(double loanSize, double interestRate, int numberOfMonths), double>(this, "MockSample", "ISample", "MonthlyPayment", "MonthlyPayment", Strictness.Lenient);
Parse = new FuncMethodMock<string, (bool returnValue, int value)>(this, "MockSample", "ISample", "Parse", "Parse", Strictness.Lenient);
}

public FuncMethodMock<(double loanSize, double interestRate, int numberOfMonths), double> MonthlyPayment { get; }
Expand Down Expand Up @@ -112,16 +115,20 @@ step is returned, we could add another step after that, provided the step we add
A step therefore accepts calls, potentially does something, and potentially forwards on to subsequent steps.

Part of the contract for a non-final step is that if they aren't assigned any furthes steps to pass on calls to,
they should behave as if they were given a ``Missing`` step. The following would return the value 120 once,
and from then on act as if the mock wasn't configured.
they should look at the strictness of the mock to decide what to do. If the strictness is 'lenient' (the default) or if it is 'strict' (what you get
if you set `Strict = true` but not `VeryStrict = true` on your ``MocklisClass`` attribute) you should just do nothing an return a default value
if a return value is asked for. However if the strictness is 'very strict' you should throw a ``MockMissing`` exception.

Note that a mock could be incompletely configured in a number of ways, consider the following:

.. sourcecode:: csharp

var mock = new MockSample();
mock.TotalLinesOfCode
.ReturnOnce(120);

The exception for the second call would look something like:
It is sufficiently configured for the first call, but the second would have to take strictness into account. The exception thrown in very strict mode for
the second call would look something like:

.. sourcecode:: none

Expand Down Expand Up @@ -167,7 +174,7 @@ If you are writing a 'final' step, implement the I-memberType-Step interface. Yo

If you are writing a non-final step, consider (as in it is very strongly recommended) subclassing the memberType-StepWithNext class, and override
the I-memberType-Step members as you see fit. If you don't override them the default behaviour is to just forward the calls on, and if you do override them you can
use 'base' to forward the call on.
use 'base' to forward the call on. Using this class also means that strictness checks are automatic.

Let's say we're writing a step to nudge our overworked developers to go home by starting to throw exceptions after 5 o'clock.

Expand Down
23 changes: 6 additions & 17 deletions source/faq/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -79,11 +79,12 @@ with steps that use ``ValueTuple``, in which case it generally pays to look for

*When my mock is called, it throws a ``MockMissingException``. But I'm absolutely certain that I did provide a mock implementation. What's going on?*

The answer is that you probably didn't add a step that would handle the call instead of passing it on to another step. Assuming this is the case...
Update: This now only happens when the MocklisClass attribute was declared with a ``VeryStrict = true`` parameter. The new behaviour (since version
0.2.0-alpha) for both lenient and strict (as in not 'very' strict) mocks is that all steps assume an implicit ``Dummy`` step for all further extension
points. In 'very strict' mode there is an implicit ``Missing`` step instead and an exception will be thrown.

When a step is added that can forward the call to a 'next' step, it will default that next step to throw the ``MockMissing`` exception. It was a judgement
call whether to have next steps default to ``Missing`` or to ``Dummy`` steps, and the reason for going with ``Missing`` was effectively that at least
you'd be told (in no uncertain terms, in the form of a ``MockMissingException``) when you have an incomplete mock implementation.
You can think of the 'very strict' mode as akin to 'treat warnings as errors'. It's a bit of a pain but it can help find issues with your mocks, and
this was thought to be important enough for this to be the default mode.

The solution is to chain a next step that does what you want the mock to do, be it ``Dummy`` step, a ``Return`` step or anything else.

Expand All @@ -103,7 +104,7 @@ The ``Log`` step will log the call, and then forward to the 'default' next step

misc.ScaleByTheAnswer.Log().Dummy();

And of course it doesn't have to be ``Dummy();`` - looking at the name of the method an appropriate mock might be ``.Func(i => c * 42);``...
And of course it doesn't have to be ``Dummy();`` - looking at the name of the method an appropriate mock might be ``.Func(i => i * 42);``...

The modifier 'readonly' is not valid...
=======================================
Expand All @@ -115,15 +116,3 @@ Resharper is what's going on... You will notice that the code compiles fine - th
`https://youtrack.jetbrains.com/issue/RSRP-473141 <https://youtrack.jetbrains.com/issue/RSRP-473141>`_

Hopefully it's fixed by the time you read this...

Mocklis.MockGenerator does not reference any other Mocklis package
==================================================================

*Why do I have to manually add references to both Mocklis.MockGenerator and Mocklis.Core? Surely the former doesn't work without the latter!*

Yes and no. The generator requires the existance of attributes and classes with the right names and namespaces, but they don't strictly speaking have
to come from a NuGet package. You can copy the code straight from the Mocklis source code into your own project and the generator wouldn't be any the
wiser, indeed if you are writing Mocklis steps and spend a lot of time debugging them this really is the way to go. If there is interest, the Mocklis
projects may well be made available in the form of Git Submodules to make this approach easier in the future.

If we enforced loading the NuGet package versions of the libraries whenever the generator was added to a project this would no longer be possible - so we don't.
25 changes: 16 additions & 9 deletions source/getting-started/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -53,17 +53,21 @@ with fields for the services we'll use.
We will at some point write proper implementations of these interfaces, but for now we want to just mock them out.

Add two new classes, ``MockConsole`` and ``MockService``. Let them implement their corresponding interface but should otherwise
stay empty. Reference the ``Mocklis.MockGenerator`` and ``Mocklis`` NuGet packages from your project (the latter which will in turn bring
in ``Mocklis.Core``), and add the ``MocklisClass`` attribute to both classes.
stay empty. Reference the ``Mocklis`` NuGet package from your project (the latter which will in turn bring
in ``Mocklis.Core`` which will bring in the code generator), and add the ``MocklisClass`` attribute to both classes.

For this walkthrough we need to add the 'Strict' parameter to the attributes - this is purely because we want to get exceptions
for missing configurations to guide us to write more mocks. In your real life cases you may wish to have all mocks return
default values instead of throwing exceptions in which case you should remove it.

.. sourcecode:: csharp

[MocklisClass]
[MocklisClass(Strict = true)]
public class MockConsole : IConsole
{
}

[MocklisClass]
[MocklisClass(Strict = true)]
public class MockService : IService
{
}
Expand Down Expand Up @@ -199,7 +203,7 @@ Let's also write out the first recorded value (in fact the only recorded value)
var mockService = new MockService();

mockConsole.ReadLine.Log().ReturnEach("8", "13", "21", string.Empty);
mockConsole.WriteLine.Log().RecordBeforeCall(out var consoleOut, a => a).Dummy();
mockConsole.WriteLine.Log().RecordBeforeCall(out var consoleOut);
mockService.Calculate.Log().Func(m => m.Sum());

var program = new Program(mockConsole, mockService);
Expand All @@ -208,9 +212,9 @@ Let's also write out the first recorded value (in fact the only recorded value)
Console.WriteLine("The value 'written' to console was " + consoleOut[0]);
}

The first parameter to ``RecordBeforeCall`` returns a list with the recorded values, and the second is a selector lambda. This is used because you may not want to record
all of the data passed around, and furthermore if any of the parameters is mutable you may want to capture the current state at the time of recording. In
this particular case we want to keep the whole thing, hence ``a => a``.
The parameter to ``RecordBeforeCall`` returns a list with the recorded values, which by default is just a list of the values passed to the method. You may want to
store a subset of these or do some calculation on some values (or if they are mutable, get the current values before they're changed) in which case you can add
a selector func as a second parameter.

The program now completes without any exceptions, with the following output:

Expand Down Expand Up @@ -304,12 +308,15 @@ Now Mocklis will generate a bit more code than normally:
[MocklisClass]
public class TypeParameters : ITypeParameters
{
// The contents of this class were created by the Mocklis code-generator.
// Any changes you make will be overwritten if the contents are re-generated.

private readonly TypedMockProvider _test = new TypedMockProvider();

public FuncMethodMock<TIn, TOut> Test<TIn, TOut>() where TOut : struct
{
var key = new[] { typeof(TIn), typeof(TOut) };
return (FuncMethodMock<TIn, TOut>)_test.GetOrAdd(key, keyString => new FuncMethodMock<TIn, TOut>(this, "TypeParameters", "ITypeParameters", "Test" + keyString, "Test" + keyString + "()"));
return (FuncMethodMock<TIn, TOut>)_test.GetOrAdd(key, keyString => new FuncMethodMock<TIn, TOut>(this, "TypeParameters", "ITypeParameters", "Test" + keyString, "Test" + keyString + "()", Strictness.Lenient));
}

TOut ITypeParameters.Test<TIn, TOut>(TIn input) => Test<TIn, TOut>().Call(input);
Expand Down
3 changes: 3 additions & 0 deletions source/installation/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ required to build the test doubles, and ``Mocklis`` which you use in your tests
for steps whose design is still under development (read: steps that simply haven't been axed yet...) and ``Mocklis.Serilog2`` which
contains a logging provider for Serilog 2.x.

Note that usually you will start off by referencing ``Mocklis`` because it references ``Mocklis.Core`` which in turn references
``Mocklis.CodeGenerator``. To this you can then add any additional packages you need such as ``Mocklis.Serilog2`` or ``Mocklis.Experimental``.

You can add the packages to your projects with the NuGet browser in Visual Studio, just make sure you have 'include prerelease'
ticked since Mocklis is still in pre-release. Search for 'Mocklis' while on the Browse tab, and you should see the Mocklis
packages and be able to add them to your projects.
Expand Down
64 changes: 53 additions & 11 deletions source/introduction/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,14 @@ The code fix replaces the contents of the class as follows:
[MocklisClass]
public class MockConnection : IConnection
{
// The contents of this class were created by the Mocklis code-generator.
// Any changes you make will be overwritten if the contents are re-generated.

public MockConnection()
{
ConnectionId = new PropertyMock<string>(this, "MockConnection", "IConnection", "ConnectionId", "ConnectionId");
Receive = new EventMock<EventHandler<MessageEventArgs>>(this, "MockConnection", "IConnection", "Receive", "Receive");
Send = new FuncMethodMock<Message, Task>(this, "MockConnection", "IConnection", "Send", "Send");
ConnectionId = new PropertyMock<string>(this, "MockConnection", "IConnection", "ConnectionId", "ConnectionId", Strictness.Lenient);
Receive = new EventMock<EventHandler<MessageEventArgs>>(this, "MockConnection", "IConnection", "Receive", "Receive", Strictness.Lenient);
Send = new FuncMethodMock<Message, Task>(this, "MockConnection", "IConnection", "Send", "Send", Strictness.Lenient);
}

public PropertyMock<string> ConnectionId { get; }
Expand Down Expand Up @@ -85,18 +88,55 @@ the ``IConnection`` interface is expected.
Can be given specific behaviour
===============================

If we just use the class that was written for us in a real test, the test would almost certainly fail. The default behaviour for a newly
constructed `mock property` is to throw an exception, such as:
If we just use the class that was written for us in a real test it might not work as expected. The default behaviour for a newly
constructed `mock property` is to return default values for any out/ref parameters and return values from methods, indexers or properties.

`Mocklis classes` are 'lenient' by default in the sense that without configuration, they will not get in your way but may also not provide you with anything useful.

You can opt-in to making your mocks stricter, so that they will throw an exception when missing configuration. This is done by adding `Strict = true` to your ``MocklisClass``
attribute.

.. sourcecode:: csharp

[MocklisClass(Strict = true)]
public class MockConnection : IConnection
{
// The contents of this class were created by the Mocklis code-generator.
// Any changes you make will be overwritten if the contents are re-generated.

public MockConnection()
{
ConnectionId = new PropertyMock<string>(this, "MockConnection", "IConnection", "ConnectionId", "ConnectionId", Strictness.Strict);
Receive = new EventMock<EventHandler<MessageEventArgs>>(this, "MockConnection", "IConnection", "Receive", "Receive", Strictness.Strict);
Send = new FuncMethodMock<Message, Task>(this, "MockConnection", "IConnection", "Send", "Send", Strictness.Strict);
}

public PropertyMock<string> ConnectionId { get; }

string IConnection.ConnectionId => ConnectionId.Value;

public EventMock<EventHandler<MessageEventArgs>> Receive { get; }

event EventHandler<MessageEventArgs> IConnection.Receive {
add => Receive.Add(value);
remove => Receive.Remove(value);
}

public FuncMethodMock<Message, Task> Send { get; }

Task IConnection.Send(Message message) => Send.Call(message);
}

Now instead of doing nothing and returning a bare minimum, the use of any mock would throw an exception instead, something like:

.. sourcecode:: none

Mocklis.Core.MockMissingException : No mock implementation found for adding a handler to Event 'IConnection.Receive'. Add one using 'Receive' on your 'MockConnection' instance.

`Mocklis classes` are 'strict' mocks in the sense that without configuration, they will not try to help you out; all calls to the mock instance will
throw a ``MockMissingException``.
Of course just doing nothing or throwing an exception doesn't help us write good tests. `Mocklis classes` are given specific behaviour using 'steps', small pieces of functionality
that are added to the `mock properties`, and can be chained together to cater for more advanced use cases.

`Mocklis classes` are given specific behaviour using 'steps', small pieces of functionality that are added to the `mock properties`, and can be
chained together to cater for more advanced use cases. The default behaviour is identical to what you would get with the ``Missing`` step.
The default behaviour is identical to what you would get with the ``Missing`` step.
The next step up (pun very much not intended) from this is the ``Dummy`` step: don't do anything, but also don't throw exceptions and use
`default` as a return value whenever one is asked for. The test that caused the error above could be mended using this ``Dummy`` step as follows:

Expand All @@ -107,7 +147,9 @@ The next step up (pun very much not intended) from this is the ``Dummy`` step: d
{
// Arrange
var mockConnection = new MockConnection();
mockConnection.Receive.Dummy();
mockConnection.Receive
.Log()
.Stored(out var registeredEvents);

// Act
var pingService = new PingService(mockConnection);
Expand All @@ -116,7 +158,7 @@ The next step up (pun very much not intended) from this is the ``Dummy`` step: d
Assert.IsNotNull(pingService);
}

The next step up from ``Dummy`` is the ``Stored`` step which will keep track of attached event handlers (and allow us to raise events on these handlers
The ``Stored`` step which will keep track of attached event handlers (and allow us to raise events on these handlers
if we wish to do so), and there are a number of other steps with other types of attachable behaviours.

This chapter is just an introduction; see the reference for a complete list of steps and other constructs used to tune how `Mocklis Classes` work.
Expand Down
16 changes: 2 additions & 14 deletions source/reference/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -179,21 +179,10 @@ Missing steps

When one of these steps is invoked, it will throw a ``MockMissingException`` with information about the `mock property` itself.

Part of the contract for writing steps that can chain on to further steps, is that if no other step has been added, we should
proceed as if a ``Missing`` step was chained instead. You can happily think of ``Missing`` steps as the 'null object' for
steps.

The exception thrown could look something like this:

*Mocklis.Core.MockMissingException: No mock implementation found for getting value of Property 'ISample.TotalLinesOfCode'. Add one using 'TotalLinesOfCode' on your 'MockSample' instance.*

You won't normally need to add these yourself to your code, as they are in essence default values, but if you ever feel the need
the syntax is simply:

.. sourcecode:: csharp

mockSample.DoStuff.Missing();

Record steps
------------

Expand All @@ -213,7 +202,7 @@ There is currently no mechanism for letting record steps share these 'ledgers' w
{
// Arrange
var mockSamples = new MockSampleWithNotifyPropertyChanged();
mockSamples.PropertyChanged.RecordBeforeAdd(out var handlingTypes, h => h.Target?.GetType()).Dummy();
mockSamples.PropertyChanged.RecordBeforeAdd(out var handlingTypes, h => h.Target?.GetType());

// Act
((INotifyPropertyChanged)mockSamples).PropertyChanged += OnPropertyChanged;
Expand Down Expand Up @@ -333,8 +322,7 @@ Verifications come in two flavours. As normal steps they check data as it passes
var vg = new VerificationGroup();
var mock = new MockSample();
mock.DoStuff
.ExpectedUsage(vg, "DoStuff", 1)
.Dummy();
.ExpectedUsage(vg, "DoStuff", 1);

... and also as 'checks' that verify some condition of an existing step:

Expand Down

0 comments on commit 4fc9524

Please sign in to comment.