Many teams have adopted some sort of test automation strategy in their development process. This concept can also be applied to custom development on the Telligent Community platform.
All Telligent Community APIs must be run as part of the Telligent Community application and cannot be executed outside of that process. This means that if you have a unit test project, it could not run any test that executes a Telligent Community API. While this may seem like a showstopper, it really isn't. After all you shouldn't be trying to test the Telligent Community APIs, you should be testing your code.
[toc]
Facades: Wrapping APIs
Custom code that directly calls the Telligent Community In-Process APIs or use the plugin model can only be executed in the Telligent Community application meaning that if you have them within your business logic you will need to abstract them out to test your code without need to call the actual API.
As a generally understood development practice, developers usually separate business logic from data access. So all of our business rules are applied in one service and actual database communication is done in another with the logic service dependent on the data service. Its a perfect separation of concerns: The business layer doesn't care how data is stored as long as it is stored somehow and the data layer doesn't care where the data came from. You can use this same pattern with the Telligent Community API. The only difference is at the data layer in your implementation, you aren't talking to the database, you are talking to the API. For example, we have written a component that requires us to get some user information from the database. We are going to do this by implementing a facade.
Step 1: Create your Own User Object
You may wonder why you don't just use the Telligent Community version. This is because in the Telligent Community API you may not be able to instantiate a particular API object and since they do not use individual interfaces, they potentially are not mockable by your test framework. Another reason is you may not actually need all of the user's information so you only expose what is needed. While this may not be a significant difference for in-process code, if you decide later to expose data via a custom REST Api, it keeps the payload down.
Here is a user object that wraps the API object. Notice in the constructor we allow the passage of an API based user but also a parameterless version so that we can use this class in unit tests. Since we only care about the UserId and The Username in our situation, that is all we are going to expose.
public class APIUser { #region Constructors public APIUser() { } public APIUser(Telligent.Evolution.Extensibility.Api.Entities.Version1.User coreAPIUser) { if (coreAPIUser != null) { UserId = coreAPIUser.Id; Username = coreAPIUser.Username; } } #endregion #region Properties public string Username{ get; set; } public int? UserId{ get; set;} #endregion }
Step 2: Design the Facade Interface and Implementation
The interface defines our wrapper service for the API. Making it an interface means when we go to unit test the business layer that depends on the APIs, we can simply mock this interface and we no longer have to worry about the Telligent Community API at all. In the implementation you can see that this service has 1 responsibility, call the API and return the expected user facade meaning that any consumers of this service are decoupled from the Telligent Community API itself.
#region Model public interface IUsersAPIFacade { APIUser GetUser(string username); } #endregion #region Implementation public class UsersAPIFacade:IUsersAPIFacade { private IUsers _usersAPI = null; public APIUser GetUser(string username) { InitializeAPI(); var user = _usersAPI.Get(new UsersGetOptions() {Username = username}); if (user != null && !user.HasErrors()) { return new APIUser(user); } return null; } #region API Init private void InitializeAPI() { if (_usersAPI == null) _usersAPI = Apis.Get<IUsers>(); } #endregion } #endregion
Step 3: Adding in a Test
We have added another simple business class the depends on our API service. Assume it has vast amounts of other validation and logic it does that are not really important to the concept being illustrated. We just want to show how we can now test this logic even though it has a dependency on the API.
This example uses Moq which is a library to mock objects and we use it to "fake" our API service. You could just as easily create a "fake" class that implements our interface, though it ends up being more cumbersome the more tests you write. If you are not familiar with Moq, what is happening is pretty straight forward. Given the IUsersAPIFacade
, it asks the mocking framework to create a class on the fly that implements that interface and for the GetUser
method given a username return me an APIUser
with that username and an Id of 12345. In this case we simply tell the dependent API service to return the data we are wanting for the success of our test since the we are testing how our login reacts to the data, not how we got the data.
#region Business Service public class CustomUserService { private IUsersAPIFacade _userApiFacade; public CustomUserService(IUsersAPIFacade userApiFacade) { _userApiFacade = userApiFacade; } public APIUser GetVaidatedUser(string username) { var user = _userApiFacade.GetUser(username); //Do some super awesome business logic that is //That is too secret to show you... return user; } } #endregion #region Tests public class UserTests { [Test] public void can_get_vaidated_user() { string _userTosearch = "wally"; var apiService = new Mock<IUsersAPIFacade>(); apiService.Setup(s => s.GetUser(It.IsAny<string>())) .Returns<string>((username) => { return new APIUser() {Username = username, UserId = 12345}; }); var businessService = new CustomUserService(apiService.Object); var foundUser = businessService.GetVaidatedUser(_userTosearch); Assert.AreSame(_userTosearch,foundUser.Username); } } #endregion
Now you may say, "what if the API does something wrong, my test won't catch that!". You would be correct and that is perfectly valid. You are in this case testing YOUR business logic and you are feeding it data that will either be good or bad depending on your test's success scenario. You are not testing the API. The documentation will provide you enough information on what to expect from the API so you can setup your test appropriately. Assuming you have you have good test coverage an error in the application will mean you have an issue with the API layer or you have not taken a scenario into account in your tests.