Skip to content

Latest commit

 

History

History
175 lines (123 loc) · 11 KB

README.md

File metadata and controls

175 lines (123 loc) · 11 KB

Beef.Test.NUnit

Unit and intra-domain integration testing framework. This capability leverages NUnit for all testing.

Intra-domain vs. inter-domain 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.


Test set up

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.


One-time set-up for the set-up fixture

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_");
    }

One-time set-up for the test fixture

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();
    }

Agent testing

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;

Mocking

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());

Additional capabilities

The following additional capabilities have been added to further aid testing:

  • ExpectException - Expects and asserts the specfied Exception type and its corresponding exception message.
  • ExpectValidationException - Expects and asserts a ValidationException 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 an int to a Guid. For example: 1.ToGuid() will return 00000001-0000-0000-0000-000000000000.
  • char.ToLongString() - Creates a long string by repeating the char for the specified count (defaults to 250). For example: 'x'.ToLongString() will return "xxxxx..." (with 250 x's).

User context

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.


Set user name

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 the ExecutionContext as the test starts.
  • AgentTester - the Create method has an overload in which the username is overridden for the test; behind the scenes this will override the ExecutionContext.

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.


Sending the user context

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.