Overview
- Integration tests give confidence in complex code interactions
- Some of Kentico's APIs are difficult to test, because they depend on a live ASP.NET application context
- We can fake this context if we know which parts are accessed during our test
- Testing Kentico's
ShoppingService
lets us explore how the E-Commerce functionality works and verify the results of our custom code
Kentico Integration Tests
Integration tests in Kentico projects are perfect for simulating interactions between classes that depend on external resources, namely the database.
Many interactions with Kentico's APIs can be performed in unit tests, but some (like UserInfoProvider.IsUserInRole
) require a real database connection ๐ค.
Other times, our code performs a complex series of steps, working with Kentico's types in specific ways defined by our business requirements.
Unit tests might not be enough to guarantee that these interactions will operate how we expect at runtime, and integration tests might be more appropriate.
You can read an example of what we might want in an integration test in my post Kentico CMS Quick Tip: Integration Testing Roles
However, there are some APIs that don't even work ๐ฏ in an integration test scenario because they depend on a live running ASP.NET application, interacting with things like HttpContext
and Session
.
Fortunately there's a solution to this predicament ๐ !
Let's look at an example.
Testing ShoppingService
Kentico provides many service types behind interfaces that are registered with its own Service Locator, Service
, found in the CMS.Core
namespace.
If we are using Dependency Injection, we could always mock these interfaces in our unit tests, but that doesn't fulfill the requirement of getting validation that the runtime interactions of multiple classes (and the database) will work as we expect.
Below we will look at running an integration test that works with ShoppingService
๐ฐ.
Creating The Test Class
First, we create a standard Kentico integration test, applying the correct NUnit
test attributes and creating a test method:
[TestFixture]
public class ShoppingServiceTests : IntegrationTests
{
[Test]
public void ShoppingService_AddItemsToCart_Will_Update_CartItems()
{
// ...
}
}
You can read more about setting up integration tests in Kentico in my post Kentico 12: Design Patterns Part 8 - Setting Up Integration Tests
Kentico 12: Design Patterns Part 8 - Setting Up Integration Tests
Sean G. Wright ใป Jul 22 '19 ใป 10 min read
#dotnet #kentico #integrationtests #testing
Our Initial Attempt
In our test method, we are going to work with CMS.Ecommerce.ShoppingService
(through the IShoppingService
interface), calling AddItemToCart()
, which we would expect to update the database, inserting a row in COM_ShoppingCartSKU
(for a new shopping cart).
Below we can see how we retrieve an ShoppingService
instance from the Service
service locator class and then immediately call AddItemToCart()
.
int skuId = 118;
int quantity = 1;
var shoppingService = Service.Resolve<IShoppingService>();
shoppingService.AddItemToCart(skuId, quantity);
var cart = shoppingService.GetCurrentShoppingCart();
cart.CartItems.Count.Should().Be(1);
This test will fail because this method requires that the execution context is a live running ASP.NET application, not a test application ๐ข.
The AddItemToCart()
method assumes there is a user associated with the current request, whether that is an anonymous user or an authenticated one.
It also assumes there is a site that the request is being processed under. That site must have a valid Kentico license to allow for the E-Commerce functionality to be accessed ๐ต.
In our test method there is no "current request", so trying to access it for a user (by checking the HttpContext.User.Identity
), or a site (by checking HttpContext.Request.Url.Host
) will fail.
The above code will throw an exception at the line shoppingService.AddItemToCart(skuId, quantity);
due to a foreign key constraint caused by the shopping cart not being persisted to the database correctly ๐คฆ๐ฟโโ๏ธ.
Faking a Live Site
So, how can we "fake" a live ASP.NET application? By faking the parts that get used by the ShoppingService
.
First, we assign a valid instance of HttpContext
to HttpContext.Current
, only filling in the values needed by the ShoppingService.AddItemToCart()
:
string siteUri = "https://localhost:44397";
string page = "/";
HttpContext.Current = new HttpContext(
new HttpRequest(page, siteUri , ""),
new HttpResponse(null))
{
User = new ClaimsPrincipal(new ClaimsIdentity())
};
The siteUri
that is listed above has to be a domain that has a valid license in the CMS (in this case, localhost
) ๐.
We also fill in the HttpContext.User
, not with anything valid, but just ensuring nothing is null
(this is due to the ShoppingService
requiring this value to not be null
internally).
Next, we use Kentico's VirtualContext
static class to assign some values that would normally be populated by the current HTTP request.
Most of the *Context
static classes source some data from VirtualContext
, so setting values here will ensure they flow to the rest of Kentico's classes ๐คฏ:
string siteCodeName = "Sandbox";
string currentUserName = "sean@wiredviews.com";
VirtualContext.SetItem(VirtualContext.PARAM_SITENAME, siteCodeName);
VirtualContext.SetItem(VirtualContext.PARAM_USERNAME, currentUserName);
I assign a real username under the VirtualContext.PARAM_USERNAME
key, so that when data is pulled from the database for the "current user", real information comes back. This user is also a valid Customer within the system.
I set the site name to match the site with a domain alias matching the siteUri
I used above ๐ง.
Now, we can write some code to validate our faked data is being passed around Kentico's libraries and giving us the state we desire.
Specifically, there should be an authenticated user in the system, and that user should be the same one I set with the VirtualContext
above ๐:
bool isAuthenticated = AuthenticationHelper.IsAuthenticated();
isAuthenticated.Should().BeTrue();
var currentUser = MembershipContext.AuthenticatedUser;
currentUser.UserName.Should().Be(currentUserName);
Now that we have our simulated live ASP.NET context within our test method we can start making calls to ShoppingService.AddItemToCart()
:
int skuId1 = 118;
int skuId2 = 200;
int quantity = 1;
var shoppingService = Service.Resolve<IShoppingService>();
shoppingService.AddItemToCart(skuId1, quantity);
var cart = shoppingService.GetCurrentShoppingCart();
cart.CartItems.Count.Should().Be(1);
shoppingService.AddItemToCart(skuId2, quantity);
cart = shoppingService.GetCurrentShoppingCart();
cart.CartItems.Count.Should().Be(2);
Above, we add an item to the cart, ensure the CartItems
count is 1
, and then add a different item, asserting the CartItems
count is 2
.
If we were to debug this code, we'd see that the value returned by shoppingService.GetCurrentShoppingCart()
would be a cart assigned to the site and Customer / User matching the username we populated in the VirtualContext
๐คฏ.
This also means that if we skip deleting the cart when the test ends, and run it again, there will already be items in the cart from the previous test run - all of this data is coming from the database ๐ค.
Pretty cool โกโก๐!
Pros and Cons
Faking a live environment can definitely be helpful, but its not easy. We have to know what dependencies the Kentico classes have - like VirtualContext
and HttpContext
- to ensure we fake the correct data.
Using a decompiler, or viewing the Kentico source code (if you have access to it), is really the only option here ๐คท๐ฝโโ๏ธ.
That said, it allows us to automate the interaction of many complex parts of our application - potentially scripting out an entire E-Commerce process from "Add To Cart", to "Payment", and finally to "Create Order" ๐ฎ.
If you've tried to test these processes before by stepping through the checkout process on the live site, manually clicking things until something succeeds or fails, an integration test like the one above might be a nice alternative ๐ค.
It also can be helpful to see exactly how different combinations of Kentico's libraries interact together and what all the different APIs really do.
An integration test is like a mini-application where we can explore ideas, but without having to pay the cost of waiting for a site to start-up with every change we make ๐.
Wrap Up
In this post we started out explaining the benefits of integration tests compared to unit tests, but realized that even integration tests can have issues when the code being tested needs to be run in an environment different than what the test simulates.
Some Kentico APIs expect a live ASP.NET request context to function correctly, which means if we want to test these we need to simulate them in our test.
Using HttpContext.Current
and VirtualContext
, we can simulate what Kentico's ShoppingService
does when a real customer adds an item to their shopping cart ๐ช.
Figuring out these hidden dependencies can be difficult, but the reward is an integration test that both helps us better understand Kentico and verifies the behavior of our custom code that uses Kentico's APIs.
In the future I'd like to expand this ShoppingService
integration test to include discounts, coupons, and creating an order from a shopping cart, so follow me to be alerted when I publish here on DEV ๐ค.
Thanks for reading ๐!
If you are looking for additional Kentico content, checkout the Kentico tag here on DEV:
Or my Kentico blog series:
Top comments (0)