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