TestFlask Weaver is made up of two nuget packages.
This is a Fody addin for TestFlask to plug in to MS Build. This package needs to be installed in order to enable TestFlask to be able to weave your methods.
You can install the package using nuget package manager inside Visual Studio or from the package manager console
Install-Package TestFlaskAddin.Fody -Version 1.0.0
When you install this package, a FodyWeaver.xml will be included in your project. You can enable by uncommenting or disable TestFlask by commenting TestFlaskAddin line using this xml file.
<?xml version="1.0" encoding="utf-8"?>
<Weavers>
<TestFlaskAddin />
</Weavers>
This package contains core TestFlask objects including the [Playback]
attribute that is used to mark your methods that are to be weaved.
You mark your methods as below.
[Playback]
public Customer GetCustomer(int customerId)
{
using (var customerClient = new CustomerServiceClient())
{
return customerClient.GetCustomer(customerId);
}
}
or using a RequestIdentifier
.
[Playback(typeof(AccountRequestIdentifier))]
public Account GetAccount(string accountNumber)
{
using (var accountClient = new ExternalAccountServices.AccountServiceClient())
{
return accountClient.GetAccount(accountNumber);
}
}
A RequestIdentifier
is class that implements IRequestIdentifier
to help TestFlask which recorded response to return for a given set of parameters of a method that is to be replayed.
An example implementation of AccountRequestIdentifier
is like this
public class AccountRequestIdentifier : IRequestIdentifier<string>
{
public string ResolveDisplayInfo(string accountNumber)
{
return $"Account ID => {accountNumber}";
}
public string ResolveIdentifierKey(string accountNumber)
{
return accountNumber;
}
}
ResolveIdentifierKey
method is the important one in this class that returns a unique string identifier value that corresponds to a set of parameter values. A hash value created out of parameter values is generally suitable for multi param or object type param methods. In this example however, accountNumber
is the only parameter for GetAccount
method and is already a unique value that distinguishes every GetAccount
request from each other.
When TestFlask weaves your methods, it does not modify your source code. It only alters your output assembly (exe or dll) in build time so that you can enable or disable TestFlask without altering your code.
Here, C# code for GetAccount
is decompiled from weaved IL code to demonstrate what TestFlask does with these marked methods behind the scenes.
public Account GetAccount(string accountNumber)
{
FuncPlayer<string, Account> funcPlayer = new FuncPlayer<string, Account>("Demo.QuickPay.Biz.ExternalAccountServices.Account Demo.QuickPay.Biz.Integrators.AccountIntegrator::GetAccount(System.String)", (IRequestIdentifier<string>) new AccountRequestIdentifier(), (IResponseIdentifier<Account>) null);
funcPlayer.BeginInvocation(accountNumber);
switch (funcPlayer.DetermineTestMode(accountNumber))
{
case TestModes.NoMock:
return funcPlayer.CallOriginal(accountNumber, new Func<string, Account>(this.GetAccount__Original));
case TestModes.Record:
return funcPlayer.Record(accountNumber, new Func<string, Account>(this.GetAccount__Original));
case TestModes.Play:
return funcPlayer.Play(accountNumber);
default:
throw new Exception("Invalid TestFlask test mode detected!");
}
}
public Account GetAccount__Original(string accountNumber)
{
using (AccountServiceClient accountServiceClient = new AccountServiceClient())
return accountServiceClient.GetAccount(accountNumber);
}
Generated method may seem complex, however it should not be a concern as it is auto generated.
What the weaved method does is more important. First thing to notice is that the original method is renamed as GetAccount__Original
and the original method GetAccount
has now some additional capabilities.
It reacts to three main TestModes
.
TestModes.NoMock
This mode is actually calling the original method inside a wrapper and your method behaves the same as the unweaved original version. It is useful when you want to ignore or bypassTestFlask in your weaved method.
TestModes.Record
In this mode, TestFlask again invokes the original method. However this time, it records all the input parameters and output values of the method inside a TestFlaskContext
. When all the weaved methods finish working and the root method returns a output, TestFlask persists this intercepted TestFlaskContext
through HTTP/Rest via TestFlask.API and store the context inside a scenario.
TestModes.Play
If you have a previously recorded scenario, then during playback, Play
mode is used and a proper recorded response from a pre-loaded TestFlaskContext
is returned from the method instead of actually calling it. If you have a RequestIdentifier
for the current method, TestFlask uses the identifier key to match and then return the proper output for that method invocation entry inside the context.
TestFlask does not intercept or replay your mutated parameter objects or values that are expected to be modified inside the method. It is a subtle but a very important behavior. TestFlask is almost always suitable for mocking functions or methods which generally do encoding/decoding data over IO. Please take notice that TestFlask is protocol independent (tcp, http or udp. It really does not matter for TestFlask). Such examples are RPC or REST service calls, database query calls and network operations that serializes some object or args over the wire and deserializes a response object back.
Parameter mutation is not something TestFlask considers to replay or provide on replay, because it makes everything utterly complex. Thus, be careful to use TestFlask on functions with immutable parameter behavior. Consider using TestFlask to mock your integration entrance methods for external code/systems. TestFlask is not suitable for weaving the methods that contains your core business logic or CPU bound complex instructions. It is not meant to weave/mock those type of functionality.*
Four main concepts are important
Name | Description |
---|---|
Project | This is virtual name for grouping scenarios. A single TestFlask.API Host can serve to multiple test projects |
Scenario | Related steps are grouped into a scenario. Such as adding items to a shopping cart and finally checking out the cart steps can be grouped in a single scenario |
Step | Http requests from the client begins a chain of calls and finally returns a response. All the call hierarchy for a single root invocation is persisted into a step |
Invocation | Every step contains a tree of recorded invocations. The very first invocation is called the root invocation. In every invocation, there are serialized request (input) and response (output) objects along with the method signature, request identifier and some display info. |
TestFlask uses custom TestFlask Http Headers to determine what to do with an incoming request.
Http Header | Description |
---|---|
TestFlask-Mode | See below |
TestFlask-ProjectKey | This is the key that you created for your backend service to categorize scenarios |
TestFlask-ScenarioNo | This is the number for your test scenario that you want to group related steps into. |
TestFlask-StepNo | This is optional on Record mode, if you provide a step no, it will override that step. If you do not, TestFlask will create an auto step under that scenario. On Play mode however, a step number must be supplied to tell TestFlask which step to replay. |
TestFlask-Mode
header can take 5 actual values but the most important ones are the first three.
TestFlask-Mode | Description |
---|---|
Record | Calls the original method and then persists request and response objects through TestFlask.API into a mongoDB database. |
Play | Calls TestFlask.API to look for a recorded response for the current request and returns that response. |
NoMock | Calls the original method with no mocking. |
Assert | Same as Play, however in this case TestFlask stores last response as an assertion result to assert later on. |
IntelliRecord | Just like record, however in this case if a matching invocation already exists it just replays it, but patches missing ones on to existing invocation tree. |
Here are some illustrations of how TestFlask works.
In the first one, there is no TestFlask involved. Web clients call the TransferMoney endpoint on the service and sequentially it invokes GetAccount operations which fetches the data from an external service, returns the response to TransferMoney. Finally, TransferMoney does its own operations and returns the final response to the client.
The second example is the Record mode. Here everything works exactly like the first time, however TestFlask intercepts these calls, creates a step under the scenario through API and puts them in TestFlask context. Just before the root invocation (which is TransferMoney
in this example) returns its response to the web clients, TestFlask records the step through TestFlask API which stores this data inside the database.
Third one is the Play mode. This is where TestFlask intervenes with the normal flow. It first intercepts incoming call to the root invocation (via HttpModule
registered in the web config), and immediately loads the given scenario and step data for the given TestFlask-StepNo in the custom Http headers using TestFlask.API. Every call from now on will be matched from TestFlask context that is loaded and no actual or external calls will be made to these matching methods.
After you build your projects with TestFlask package, your service is mostly ready to record incoming requests with TestFlask headers and replay for a given step. However a test-flask ready (weaved) service should better be hosted in a different test environment rather than your dev environment in order to keep an alive test host to use for all the development team.
A configuration for the TestFlask ready service is as follows;
Register testFlask config section inside your web.config and configure TestFlask api address with a pre defined project key for your service
<configSections>
...
<section name="testFlask" type="TestFlask.Aspects.Config.TestFlaskConfigSection, TestFlask.Aspects, PublicKeyToken=5b9114f222369bea, Version=1.0.0.0" />
</configSections>
<testFlask>
<api url="http://myTestFlaskApiHostUrl:12079/" />
<project key="DemoQuickPay" />
</testFlask>
So you need a TestFlask.API ready and hosted somewhere that your service can call through Http. For more information on the API, go to TestFlask.API page.
An Http module should also be registered to intercept raw http request and create an auto step in Record mode.
<modules runAllManagedModulesForAllRequests="true">
...
<add name="TestFlaskHttpModule" type="TestFlask.Aspects.Context.TestFlaskHttpModule, TestFlask.Aspects, PublicKeyToken=5b9114f222369bea, Version=1.0.0.0" />
</modules>