Testing - quandis/qbo3-Documentation GitHub Wiki

QBO uses several tools to automate testing:

  • xUnit for any new unit and integration tests of server-side components
  • Microsoft.VisualStudio.TestTools.UnitTesting from legacy unit and integration tests of server-side components
  • Jasmine for integration testing of client configuration and the user experience (UX), and
  • loader.io for load and concurrency testing of server farms

Quandis strives for 100% unit test coverage of server-side components developed and maintained by Quandis. These tests are part of our CI/CD architecture. Integration testing is more problematic, as QBO is typically used as a service layer for coordinating multiple systems, leaving QBO beholden to the 'testability' of third party systems. We approach testing of third party interfaces on a case-by-case basis.

Quandis strongly recommends that QBO clients who take ownership of their own QBO configuration create QBO Jasmine tests to cover their specific configuration (workflows, documents, etc.), and includes these Jasmine tests as part of their own CI/CD process.

Server-side Tests

Server-side unit and integration tests should derive from the qbo.Test.Helpers.BaseTest class:

[TestClass]
public class SomeClass: BaseTest
{
  [TestClass]
  public class SomeMethod: SomeClass 
  {
    [TestMethod]
    public async Task SomeExpectation() 
    {
      // test code goes here
    }
  }
}

In order to leverage BaseTest, you must have:

  • a local installation of a QBO system located in c:\inetpub\wwwroot
  • a working QBO database (installed from the qbo.Db.sln)
  • IIS running a website against c:\inetpub\wwwroot

BaseTest provides sugar to facilitate testing. QBO test projects use post-build execution scripts to copy the configuration settings for the QBO site configured at c:\inetpub\wwwroot, including connectionStrings.config so we have a known database to test against.

It is possible to run a local website using a remote database (on another subnet, like AWS or Azure). Don't - the latency will drive you batty. Installing a SqlExpress copy of qbo.Db.sln should take you no more than 5 minutes.

TransactionScope: Data Cleanup

The BaseTest class creates a TransactionScope on initialization, and issues a rollback on tear down, meaning that any data you write to the QBO database during the course of testing will be rolled back.

There are use cases where the TransactionScope presents a problem, such as reading from Excel using ADO.NET (Excel does not support transactions). In such cases, you can disable the transaction scoping for the portion of test code that present the problem:

using (var scope = new TransactionScope(TransactionScopeOption.Suppress))
{
  // test code that does not support transactions
}

Mocking Configuration

Frequently, tests need custom configuration settings. BaseTest provides sugar for this:

MockConfiguration<T>(DbConfigurationElementCollection<T> collection, T instance, string name = null)

where T can be any configuration element based on QBO's DbConfigurationElementCollection pattern, including:

  • Credentials
  • EncryptionKeys
  • FileObjects

Mock Credential

// Add a mock credential that QBO plugins can use
var config = ConfigurationManager.GetSection("qbo/Credentials") as CredentialConfiguration;
var credential = MockConfiguration(config.CredentialCache, new Application.Configuration.Credential() { 
  UriPrefix = "http://some.thirdparty.api.io/", 
  Username = "someUserName", 
  Password = "someSecret", 
  AuthType = "Basic" 
});

Mock Encryption Key

var section = BaseConfiguration<EncryptionKeyConfiguration>.Load("qbo/EncryptionKeys");
var keyName = GetRandomString();
MockConfiguration(section.EncryptionKeys, new EncryptionKey()
{
  Name = keyName,
  KeyUri = "mock_secret",
  Type = typeof(MockEncryptionKey)
}, keyName);

Mocking Object Configuration

The ObjectConfiguration class represents a QBO module, and comprises several nested configuration collections, including Statements, Filters, Services, Children and more. The qbo.TestHelpers modules provides ObjectConfiguration extensions to mock these nested configuration elements.

var contact = new ContactObject(User);
var statement = contact.Configuration.MockStatement(new DbStatement()
{
	Name = "MyMockStatement",
	Query = "SELECT TOP 1 * FROM Contact; SELECT TOP 1 * FROM ConfigurationEntry",
	ReturnType = OperationReturnType.DataSet
});
var dataset = contact.ExecuteDataSetAsync("MyMockStatement", "".ToProperties());

Mocking Application Settings

Application settings can be mocked in memory for unit testing:

ApplicationSettingsBase.Override(string property, object current, object desired)

Examples:

// Override the Attachment module's AppendFileObject setting
Properties.Settings.Default.Override("AppendFileObject", Properties.Settings.Default.AppendFileObject, "MyFileObject");
// Override the Message module's SetTextFromHtml boolean setting
Properties.Settings.Default.Override("SetTextFromHtml", Properties.Settings.Default.SetTextFromHtml, true);

Mocking Web Responses

Web responses can be mocked using two classes from qbo.Test.Helpers: MockSyncResponse and MockAsyncResponse. MockSyncResponse provides a mocked synchronous response, and MockAsyncResponse provides a mocked asynchronous response.

Creating Mock Responses

Each class has a SetupMockResponse() method that returns a fake WebRequest factory object.

SetupMockResponse has 3 parameters:

  • uri: a string containing the URI that will prompt the fake response
  • responseText: a string containing the Body to be returned by the fake response
  • headers: a WebHeaderCollection containing the Headers to be returned by the fake response

Examples:

//Create a mock asynchronous response with only a Body
string uri = "www.google.com";
string expectedResult = "testing";
MockAsyncResponse mockResponse = new MockAsyncResponse();
var factory = mockResponse.SetupMockResponse(uri, expectedResult, null);

//Create a mock asynchronous response with Headers and a Body
string uri = "www.google.com";
string body = "testing";
string headerName = "ContentType";
string expectedHeaderValue = "text / html";
WebHeaderCollection headers = new WebHeaderCollection();
headers.Add(headerName, expectedHeaderValue);
MockAsyncResponse mockResponse = new MockAsyncResponse();
var factory = mockResponse.SetupMockResponse(uri, body, headers);

Exercising mock responses

The factory object returned by mock response must be exercised using the Create() method, which returns a mock Request object.

The mock Request object then responds to either GetResponseAsync() in the case of an asynchronous response, or GetResponse() in the case of a synchronous response.

Examples:

The following examples demonstrate the use of these mock objects in actual test cases:

Insert, exercise, and capture mock Header data from a synchronous response:

//Arrange
string uri = "www.google.com";
string body = "testing";
string contentTypeKey = "ContentType";
string contentTypeValue = "text / html";
WebHeaderCollection headers = new WebHeaderCollection();
headers.Add(contentTypeKey, contentTypeValue);
MockSyncResponse mockResponse = new MockSyncResponse();
var factory = mockResponse.SetupMockResponse(uri, body, headers);

//Act
var actualHeaders = factory.Create(uri).GetResponse().Headers;

//Assert
Assert.AreEqual(contentTypeValue, actualHeaders.Get(contentTypeKey));

Insert, exercise, and capture mock Header data from an asynchronous response:

//Arrange
string uri = "www.google.com";
string body = "testing";
string headerName = "ContentType";
string expectedHeaderValue = "text / html";
WebHeaderCollection headers = new WebHeaderCollection();
headers.Add(headerName, expectedHeaderValue);
MockAsyncResponse mockResponse = new MockAsyncResponse();
var factory = mockResponse.SetupMockResponse(uri, body, headers);

//Act
var actualRequest = factory.Create(uri);
var response = await actualRequest.GetResponseAsync();
var actualContentTypeValue = response.Headers.Get(headerName);

//Assert
Assert.AreEqual(expectedHeaderValue, actualContentTypeValue);

Insert, exercise, and capture a Body from an asynchronous response:

    public class AsyncTestData : MockAsyncResponse
    {
        public static async Task<String> InvokeWebService(string uri, IHttpWebRequestFactory factory)
        {
            var actualRequest = factory.Create(uri);
            var response = await actualRequest.GetResponseAsync();
            string result = null;
            using (var stream = response.GetResponseStream())
            {
                using (var reader = new StreamReader(stream))
                {
                    result = await reader.ReadToEndAsync();
                }
            }
            return result;
        }
    }

    [TestClass]
    public class MockAsyncResponseTests
    {
        [TestMethod]
        public async Task MockResponseShouldReturnExpectedBodyAsync()
        {
            //Arrange
            string uri = "www.google.com";
            string expectedResult = "testing";
            MockAsyncResponse mockResponse = new MockAsyncResponse();
            var factory = mockResponse.SetupMockResponse(uri, expectedResult, null);

            //Act
            var actualResult = default(string);
            actualResult = await AsyncTestData.InvokeWebService(uri, factory);

            //Assert
            Assert.AreEqual(expectedResult, actualResult);
        }
    }

Insert, exercise, and capture a Body from a synchronous response:

    public class SyncTestData : MockSyncResponse
    {
        public static string InvokeWebService(string uri, IHttpWebRequestFactory factory)
        {
            var actualRequest = factory.Create(uri);
            actualRequest.Method = WebRequestMethods.Http.Get;
            string actual;
            using (var httpWebResponse = (HttpWebResponse)actualRequest.GetResponse())
            {
                using (var streamReader = new StreamReader(httpWebResponse.GetResponseStream()))
                {
                    actual = streamReader.ReadToEnd();
                }
            }
            return actual;
        }
    }

    [TestClass]
    public class MockSyncResponseTests
    {
        [TestMethod]
        public void MockResponseShouldReturnExpectedBodySync()
        {
            //Arrange
            string uri = "www.google.com";
            string expectedResult = "testing";
            MockSyncResponse mockResponse = new MockSyncResponse();
            var factory = mockResponse.SetupMockResponse(uri, expectedResult, null);

            //Act
            var actualResult = SyncTestData.InvokeWebService(uri, factory);

            //Assert
            Assert.AreEqual(actualResult, expectedResult);
        }
    }
⚠️ **GitHub.com Fallback** ⚠️