Skip to content

Latest commit

 

History

History
 
 

Beef.Test.NUnit

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

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 may be a 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 setting the defaults for the local Reference Data (TestSetUp.SetDefaultLocalReferenceData) is initiated. An example is as follows:

[SetUpFixture]
public class FixtureSetUp
{
    [OneTimeSetUp]
    public void OneTimeSetUp()
    {
        TestSetUp.DefaultEnvironmentVariablePrefix = "Hr";
        TestSetUp.SetDefaultLocalReferenceData<IReferenceData, ReferenceDataAgentProvider, IReferenceDataAgent, ReferenceDataAgent>();
        TestSetUp.DefaultExpectNoEvents = true;
        var config = AgentTester.BuildConfiguration<Startup>();

        TestSetUp.RegisterSetUp(async (count, _) =>
        {
            return await DatabaseExecutor.RunAsync(new DatabaseExecutorArgs(
                count == 0 ? DatabaseExecutorCommand.ResetAndDatabase : DatabaseExecutorCommand.ResetAndData, config["ConnectionStrings:Database"],
                typeof(Database.Program).Assembly, Assembly.GetExecutingAssembly()) { UseBeefDbo = true } ).ConfigureAwait(false) == 0;
        });
    }
}

One-time set-up for the test class

Within the OneTimeSetUp for each TestFixture the TestSetUp.Reset should be invoked; this will (re)invoke the registered set up (TestSetUp.RegisterSetUp). Example as follows

[TestFixture, NonParallelizable]
public class PersonTest
{
    [OneTimeSetUp]
    public void OneTimeSetUp()
    {
        TestSetUp.Reset();
    }

Or, alternatively, by using the TestSetUpAttribute on a test method this will perform the same function.


Agent testing

As beef is largely about accelerating API development this testing capability further enables and simplifies the testing of APIs directly. 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 Test method to simplify the construction enabling 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.Test<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.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.Test<RobotAgent, Robot>()
    .ExpectStatusCode(HttpStatusCode.OK)
    .ExpectChangeLogUpdated()
    .ExpectETag(v.ETag)
    .ExpectUniqueKey()
    .ExpectEventWithValue("Demo.Robot.*", "Update")
    .ExpectValue((t) => v)
    .Run(a => a.UpdateAsync(v, 1.ToGuid()));

Mocking

As stated earlier, the likes of any cross domain (inter-domain) dependencies should be mocked out. Or any other layer within the Beef to support a specific testing use case. To support this, both dependency injection (DI) and the Moq framework is leveraged (or alternatively, any mocking framework can be used if required).

// Create the mock object (not use of the ReturnsWebApiAgentResultAsync helper in this scenario)
Mock<IPersonAgent> mock = new Mock<IPersonAgent>();
mock.Setup(x => x.GetAsync(1.ToGuid(), null)).ReturnsWebApiAgentResultAsync(new Person { LastName = "Mockulater" });

// Replace the existing scoped item with the mock object.
var svc = new Action<Microsoft.Extensions.DependencyInjection.IServiceCollection>(sc => sc.ReplaceScoped<IPersonAgent>(mock.Object));

// Use the alternative light-weight WebApplicationFactory (WAF) as the agent tester (optional).
using var agentTester = Beef.Test.NUnit.AgentTester.CreateWaf<Startup>(svc);
      
// Execute the test.      
agentTester.Test<PersonAgent, string>()
    .ExpectStatusCode(HttpStatusCode.OK)
    .ExpectValue(_ => "Mockulater")
    .Run(a => a.InvokeApiViaAgentAsync(1.ToGuid()));

Validation testing

To simplify the testing of validations, and limit the need to have the backing data source (by mocking) to improve testing run time performance, the ValidatorTester manages the testing of a validator outside of an API execution context with integrated mocking of services as required. The ValidationTester uses a similar assert (expect) approach to enable with integrated service mocking.

The following is an example excerpt from EmployeeValidatorTest:

var eds = new Mock<IEmployeeDataSvc>();
eds.Setup(x => x.GetAsync(1.ToGuid())).ReturnsAsync(new Employee { Termination = new TerminationDetail { Date = DateTime.UtcNow } });

ValidationTester.Test()
    .OperationType(Beef.OperationType.Update)
    .AddScopedService(_referenceData)
    .AddScopedService(eds)
    .ExpectErrorType(Beef.ErrorType.ValidationError, "Once an Employee has been Terminated the data can no longer be updated.")
    .Run(() => EmployeeValidator.Default.Validate(e));

Additional capabilities

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

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 Test 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.

An example of sending the user as a header is as follows; this code should be replaced with a bearer token in the form of a JWT for example:

AgentTester.RegisterBeforeRequest(r => r.Headers.Add("cdr-user", Beef.ExecutionContext.Current.Username));