Unit and intra-domain integration testing framework. This capability leverages NUnit for all testing.
Intra-domain essentially means within (isolated to) the domain itself; excluding any external domain-based dependencies. For example a Billing domain, may be supported by a SQL Server Database for data persistence, and as such is a candidate for inclusion within the testing.
However, if within this Billing domain, there is an Invoice entity with a CustomerId attribute where the corresponding Customer resides in another domain (external domain-based dependency) which is called to validate existence, then this should be excluded from within the testing. In this example, the cross-domain invocation should be mocked-out as it is considered Inter-domain.
In summary, Intra- is about tight-coupling, and Inter- is about loose-coupling.
Before a test executes, there is the requirement to perform set up activities, such as a test data source, etc. for example. The TestSetUp
and TestSetUpAttribute
enable.
Within the OneTimeSetUp
for the SetUpFixture
the TestSetUp.RegisterSetUp
enables the configuration of the set up (including that which is re-invoked with the one-time set-up for the test fixture). Other set up logic including the starting of the API test server via AgentTester.StartupTestServer
is initiated. Example as follows:
[SetUpFixture]
public class FixtureSetUp
{
[OneTimeSetUp]
public void OneTimeSetUp()
{
TestSetUp.RegisterSetUp((count, data) =>
{
return DatabaseExecutor.Run(
count == 0 ? DatabaseExecutorCommand.ResetAndDatabase : DatabaseExecutorCommand.ResetAndData,
AgentTester.Configuration["ConnectionStrings:BeefDemo"],
typeof(DatabaseExecutor).Assembly, typeof(Database.Program).Assembly, Assembly.GetExecutingAssembly()) == 0;
});
AgentTester.StartupTestServer<Startup>(environmentVariablesPrefix: "Beef_");
}
Within the OneTimeSetUp
for each TestFixture
the TestSetUp.Reset
should be invoked; this will flush all caches, clear out any mock objects, and re-invoke the registered set up (TestSetUp.RegisterSetUp
). Example as follows:
[TestFixture, NonParallelizable]
public class PersonTest
{
[OneTimeSetUp]
public void OneTimeSetUp()
{
TestSetUp.Reset();
}
As beef is largely about accelerating API development this testing capability further enables and simplifies the testing of APIs. The philosophy of this testing is to exercise the APIs end-to-end, including over the wire transport and protocols, and tightly-coupled backend data sources where applicable (intra-domain).
The AgentTester
provides a means to invoke an API and assert (expect) a given response to be considered valid. The AgentTester
invokes the API via its corresponding Service Agent. The advantage of this is that the HTTP request/response, HTTP headers, URL parameterisation, etc. are verified, as well as the underlying intra-domain business logic and corresponding data services.
The AgentTester
has a Create
method to simplify the construction to enable fluent-style method-chaining to assert (expect) and execute a selected API operation; these asserts are as follows:
Method | Description |
---|---|
ExpectStatusCode |
Expect a response with the specified HttpStatusCode . |
ExpectErrorType |
Expect a response with the specified ErrorType . |
ExpectMessages |
Expect a response with the specified messages. |
ExpectNullValue |
Expect null response value. |
ExpectValue |
Expect a response comparing the specified values (supports ignoring of specified properties). |
IgnoreChangeLog |
Ignores the IChangeLog property. |
ExpectChangeLogCreated |
Expects the IChangeLog property to be implemented for the response with generated values for the underlying CreatedBy and CreatedDate matching the specified values. |
ExpectChangeLogUpdated |
Expects the IChangeLog property to be implemented for the response with generated values for the underlying UpdatedBy and UpdatedDate matching the specified values. |
IgnoreETag |
Ignores the IETag property. |
ExpectETag |
Expects the IETag to be implemented for the response with a generated value different to the previous value. |
ExpectUniqueKey |
Expects the IUniqueKey to be implemented for the response with a generated value. |
ExpectEvent |
Expects an event is published (in order specified). The expected event can use wildcards for EventData.Subject and optionally define EventData.Action . An EventData.Value can be optionally specified including any corresponding members to igore for the comparison. Finally, the remaining EventData properties are not compared. Once an event is speficied then all expected events must be specified. |
ExpectEventWithValue |
Same as ExpectEvent above defaulting the EventData.Value to the return value. |
ExpectNoEvents |
Expects that no Event was published. |
An example usage is as follows (see PersonTest
for more complete usage):
[Test, TestSetUp]
public void A140_Validation_ServiceAgentInvalid()
{
AgentTester.Create<PersonAgent, Person>()
.ExpectStatusCode(HttpStatusCode.BadRequest)
.ExpectErrorType(ErrorType.ValidationError)
.ExpectMessages(
"First Name must not exceed 50 characters in length.",
"Last Name must not exceed 50 characters in length.",
"Gender is invalid.",
"Eye Color is invalid.",
"Birthday must be less than or equal to Today.")
.Run((a) => a.Agent.UpdateAsync(new Person() { FirstName = 'x'.ToLongString(), LastName = 'x'.ToLongString(), Birthday = DateTime.Now.AddDays(1), Gender = "X", EyeColor = "Y" }, 1.ToGuid()));
}
Another example usage is as follows (see RobotTest
for more complete usage):
AgentTester.Create<RobotAgent, Robot>()
.ExpectStatusCode(HttpStatusCode.OK)
.ExpectChangeLogUpdated()
.ExpectETag(v.ETag)
.ExpectUniqueKey()
.ExpectEventWithValue("Demo.Robot.*", "Update")
.ExpectValue((t) => v)
.Run((a) => a.Agent.UpdateAsync(v, 1.ToGuid())).Value;
As stated earlier, for the likes of cross domain (inter-domain) dependencies should be mocked out. To support this, the Moq framework is leveraged. Additional support is added to integrate into the Beef Factory
instantiation capability.
The TestSetUp.CreateMock<T>()
method will create the Moq Mock
object and set within the beef Factory
; for example:
UnitTestSetup.CreateMock<IPersonManager>().Setup(x => x.GetAsync(It.IsAny<Guid>())).ReturnsAsync(new Person());
The following additional capabilities have been added to further aid testing:
ExpectException
- Expects and asserts the specfiedException
type and its corresponding exception message.ExpectValidationException
- Expects and asserts aValidationException
and its corresponding messages.DependencyGroupAttribute
- Provides a means to manage a group of test executions such that as soon as one fails the others within the dependency group will not execute as a success dependency is required.
The following extension methods have beed added to aid testing:
int.ToGuid()
- Converts anint
to aGuid
. For example:1.ToGuid()
will return00000001-0000-0000-0000-000000000000
.char.ToLongString()
- Creates a longstring
by repeating thechar
for the specified count (defaults to 250). For example:'x'.ToLongString()
will return"xxxxx..."
(with 250x
's).
Within an API execution the user context should be defined to ensure that the likes of authentication and authorisation are performed for a request. This user context needs to be passed from the consumer (the test agent) to the service (API).
For Beef the ExecutionContext
houses the Username
for the request; additional properties can be added as required. The ExecutionContext
is used both within the consumer, as well as within the service processing. The same instance is not used (shared) between the two. The ExecutionContext
is essentially internal to Beef execution only.
User context is typically passed using the likes of JWTs as an HTTP header on the request. The Beef testing framework enables the opportunity for this to occur.
There are two opportunities to set the username for a test (specifically for the consumer):
TestSetUpAttribute
- this has an overload in which the username is set for the test; behind the scenes this will set theExecutionContext
as the test starts.AgentTester
- theCreate
method has an overload in which the username is overridden for the test; behind the scenes this will override theExecutionContext
.
Each of the above have overloads that take an object userIdentifer
to support consts or enum values. The AgentTester.UsernameConverter
function enables logic to be added to convert the identifier to a corresponding username.
Where the username is not set it will default to the AgentTester.DefaultUsername
. By default the value is Anonymous
.
There is nothing in Beef that by default will send the user context for an API; this is the responsibility of the developer to implement as there is no standard approach.
To access each HTTP request before it is sent the AgentTester.RegisterBeforeRequest
action should be set. This is passed the HttpRequestMessage
which should be updated as required. The ExecutionContext.Current.Username
should be used.