The purpose of unit testing is to ensure that the building blocks of an application work they way they are intended. If the application is built in large, monolithic blocks, it’s difficult to verify the individual pieces. Similarly, a portion of the code that depends on external services is difficult to test — external services might not be available (leading to false failed tests), might not respond well to being hit as often as tests should be run, or might just slow down our tests.

By organizing code for testing, we can provide a solid foundation for the application itself. Apply these principles:

  • Build cohesive functions in libraries.
  • Use dependency injection to avoid external dependencies.

Note that these principles apply broadly to unit testing software, not just MarkLogic.

Build Cohesive Functions

If a single function includes complex logic to accomplish several distinct tasks, it’s difficult to write tests that verify that each of them work correctly. We may be able to write tests that assert correct results for inputs and outputs of that complex function, but if the assertions fail, it’s often difficult to track down why they failed. By segmenting complex functionality into smaller chunks, we can verify that the smaller chunks do what they should as well as verifying that the larger function builds the correct result from the smaller chunks. Consider this code:

function buildComplexResult(input1, input2, input3) {
  let partialResult = /* complex logic using input1, input2 */
  let secondaryResult = /* complex logic using partialResult, input3 */
  let finalResult = /* complex logic using secondaryResult */
  return finalResult;
}

To test this function, we’d need to call it many times with lots of different inputs, each making assertions about the finalResult. Suppose some of those assertions fail. Where is the problem? As written, this can be difficult to determine. Let’s refactor and take another look:

function buildPartial(input1, input2) {
  return /* complex logic */
}

function buildSecondary(partial, input3) {
  return /* complex logic */
}

function buildFinal(secondary) {
  return /* complex logic */
}

function buildComplexResult(input1, input2, input3) {
  let partialResult = buildPartial(input1, input2);
  let secondaryResult = buildSecondary(partialResult, input3);
  let finalResult = buildFinal(secondaryResult);
  return finalResult;
}

TODO

Testing Endpoints

In MarkLogic, code can either be in a library or a main module. A main module is intended to be run as a whole. A library comprises a set of functions, each of which can be tested separately. This means we can be more granular in our testing when working with libraries.

Service endpoints in MarkLogic need to do things like get HTTP parameters, request bodies, even headers. While it’s important to test that these are gathered correctly, they are separate from the business logic that will operate on those inputs.

Build your endpoints to gather inputs as needed (this will be different in REST API extensions than it will in main-module endpoints) and pass them to a function to do the real work. This allows us to build tests that focus on the logic. We can use separate tools (either outside of MarkLogic or by using xdmp.httpGet and similar) to run endpoint-level tests.

Use Dependency Injection

TODO