-
Notifications
You must be signed in to change notification settings - Fork 0
TestJsonAttribute
Especially when the developer needs to create a large number of test cases, the organization/management of these test cases can be daunting. And while it is possible to hard-code test input parameters and expected values, this practice often leads to substantial lines of code in unit/integration tests.
To keep unit/integration tests as streamlined as possible, the developer should externalize the test input parameters and expected values. One option is to place test parameters and expected results in individual or structured files in the test project. Such an approach provides a significant improvement in test case organization; however, creating and editing the files can be a daunting task, especially if multiple test cases are included in a single file.
Another option is to put test parameters and expected results in a database in a format that is easily queried. This is the approach taken by the current library. The EDennis.NetCoreTestingUtilities library only addresses retrieval of test records during unit/integration testing. For creation of test records, one additional library is needed:
- EDennis.MigrationsExtensions -- which creates a TestJson table and stored procedures for inserting and viewing test records
The TestJsonAttribute class provides developers with the ability to obtain test input parameters and expected results from a database. The class works with XUnit and SQL Server.
The TestJsonAttribute class and its supporting classes do several things behind the scenes, including:
- Retrieving and caching one or more set of test records from a TestJson table
- Building a parameterized query from the TestJsonAttribute parameters supplied to a test method
- Retrieving all records (input parameters and expected result) for a particular test case
- Supplying typed input parameters and expected results as structured data back to test methods
- Deserializing data from test records into typed objects, arrays, or primitive values
- Throwing informative exceptions when one or more test records are missing from the database
The TestJsonAttribute class extends Xunit's DataAttribute class, which provides developers with a way of supplying parameterized test cases to test methods. The constructor for the TestJsonAttribute class takes a large number of parameters:
Name | Description |
---|---|
string databaseName |
The name of the database (often the same as the application's database) |
string projectName |
The name of the project under test |
string className |
The name of the class under test |
string methodName |
The name of the method under test |
string testScenario |
A top-level category or significant variation of the test |
string testCase |
A label for the repetition of a test with different input parameters |
string serverName |
the name of the database server (default: (LocalDb)\MSSQLLocalDb) |
string testJsonSchema |
the name of test schema (default: _) |
string testJsonTable |
the name of test table (default: TestJson) |
TestJsonAttribute requires a large number of constructor arguments, most of which will have the same value for test methods in the same test class. For this reason, it is often helpful to create one or more subclasses of TestJsonAttribute where most of the constructor arguments are hard-coded. In essence, this is partial application applied to a constructor.
As a strategy, the developer could embed within a test class an internal subclass of TestJsonAttribute where all but the methodName, testScenario, and testCase arguments are hard-coded.
public class SomeTestClass {
//...
//note: this class can be called anything
internal class TestJson_ : TestJsonAttribute {
public TestJson_(string methodName, string testScenario, string testCase)
: base("ColorDb", "EDennis.Samples.ColorDb", "ColorRepo",
methodName, testScenario, testCase, \* defaults for other params *\) {
}
}
//... test methods to follow
}
Each Xunit test method will have a '[Theory]attribute and one instance of
[TestJson_]` (or whatever subclass has extended TestJsonAttribute) for every test scenario/test case combination.
The arguments to [TestJson_]
(the subclassed [TestJson]
) will be
(a) the name of the method under test (b) the name of the test scenario (which could be an empty string, if not needed) (c) the name of the test case (which could be arbitrary, such as "A" or "B," or meaningful, such as the value of a primary or business key).
Test method parameters for a method using [TestJson]
or [TestJson_]
are always the following:
Name | Description |
---|---|
string t |
A label for the test case (auto-generated). t is used to make Test Explorer nodes more compact |
JsonTestCase jsonTestCase |
An object holding an array input parameters and expected results |
Developers familiar with XUnit [InlineData]
may question the discrepancy between the [TestJson_]
arguments and the test method parameters. (With [InlineData]
there is a correspondence between attribute arguments and method parameters). In reality, the discrepancy actually makes things easier for the developer. The JsonTestCase class has a GetObject method that makes it trivial to retrieve the expected result and all of the input parameters and cast these values to the appropriate types.
[Theory]
[TestJson_("MethodA", "TestScenarioA", "TestCaseA")]
[TestJson_("MethodA", "TestScenarioA", "TestCaseB")]
[TestJson_("MethodA", "TestScenarioB", "TestCaseA")]
[TestJson_("MethodA", "TestScenarioB", "TestCaseB")]
public void MethodA(string t, JsonTestCase jsonTestCase) {
Output.WriteLine($"Test case: {t}"); //write test case label to output
var input = jsonTestCase.GetObject<int>("Input"); //retrieve an input parameter
var expected = jsonTestCase.GetObject<List<Person>>("Expected"); //retrieve expected result
var actual = Repo.GetById(input); //using EDennis.AspNetCore.Base
Assert.True(actual.IsEqualOrWrite(expected, PROPS_TO_IGNORE, Output)); //using EDennis.NetCoreTestingUtilities
}
The success of the TestJson approach relies upon the creation of JSON objects or arrays as expected results and sometimes as test inputs (e.g., when testing creation or updating of records). There are several ways to create these JSON objects or arrays. For example, one could ...
- Hand-code the JSON. For simple JSON structures, this could be relatively easy to do; however, for more complicated objects or arrays, this approach could quickly become cumbersome.
- Save the actual results of a test as JSON for use as the expected results. This approach is very easy to do, but it does introduce risk that the expected results are not accurate.
- Generate JSON from test data using a tool. For example, if the developer has a set of base test data stored in a SQL Server database, the developer could use the built-in FOR JSON clause to generate JSON.
- EDennis.MigrationsExtensions provides some stored procedures that make inserting JSON data (and even primitive values) into a TestJson table easier.
- There are many examples of this FOR-JSON approach in the sample projects in EDennis.AspNetCore.Base.
- NOTE: Both in this solution and in EDennis.AspNetCore.Base, there is an included PowerShell script (ExecuteTestSql.ps1) that allows developers to execute all .sql files in all test projects in a solution. This script is especially helpful when the test cases are stored in LocalDb and a developer other than the author of the .sql files wishes to run unit/integration tests that use test data from the database.
-- from EDennis.Samples.Hr.InternalApi.Tests in EDennis.AspNetCore.Base solution
use hr;
declare @FirstName varchar(30) = 'Curly'
declare @Id int
select @Id = max(id) + 1 from Employee
declare @Input varchar(max) =
(
select @FirstName FirstName
for json path, include_null_values, without_array_wrapper
);
begin transaction
insert into Employee(FirstName)
select @FirstName;
declare @Expected varchar(max) =
(
select * from Employee
where Id = @Id
for json path, include_null_values, without_array_wrapper
);
rollback transaction
exec _.ResetIdentities
exec _.SaveTestJson 'EDennis.Samples.Hr.InternalApi1', 'EmployeeRepo', 'Create','CreateAndGet',@FirstName,'Id', @Id
exec _.SaveTestJson 'EDennis.Samples.Hr.InternalApi1', 'EmployeeRepo', 'Create','CreateAndGet',@FirstName,'Input', @Input
exec _.SaveTestJson 'EDennis.Samples.Hr.InternalApi1', 'EmployeeRepo', 'Create','CreateAndGet',@FirstName,'Expected', @Expected
exec _.GetTestJson 'EDennis.Samples.Hr.InternalApi1', 'EmployeeRepo', 'Create','CreateAndGet',@FirstName