How to test a service that only connects other services

  softwareengineering

I’m currently scratching my and my colleagues head about whether and how we could test SyncService::syncFoo in any meaningful way
that does not involve basically recreating the whole call tree as mocks.

Edit: It is not a microservice its a Service Class within one project – there was some confusion in the answers/comments

We could Mock all three dependencies and make sure the right Message is passed to the Queue but that seems useless as we would reflect the implementation 1:1 in the test.
The example code is a bit stripped down but it represents what we want to achieve very nicely.

Should we use a completely different approach? Is it ok to NOT test the sync service?

// this should be the signature we want to call
SyncService.syncFoo(Entity)

we now want to:

  • Get a list of endpoints our message should be delivered to based on the passed entity
  • create a message based on the passed entity
  • dispatch the message using the queue

Our current basic approach:

Class MsgQueue 
{
  // tested
  public dispatch(msg, endpoints[])
}

Class MsgFactory
{
  // tested
  create(Entity)
}

Class EndpointRepository()
{
  // tested
  getEndpoints(Entity)
}

Class SyncService()
{
    MsgQueue
    MsgFactory
    EndpointRepository

    public sync(Entity) 
    {
        endPoints = this.EndpointRepository.getEndPoints(Entity)

        if endPoints.empty() 
        { 
           return
        }

        msg = this.MsgFactory.create(Entity)
        
        this.MsgQueue.dispatch(msg, endPoints)
    }
}

9

Given that you have very limited logic in this service I think mocking is a reasonable approach.

Essentially the output of the function is the dispatch call, you could break this out to make testing more obvious, or just check that the mock method is called with the correct arguments.

Yes there will be more mock setup than code tested. But that’s just the nature of your code here.

The other approach would be to upgrade to an integration test. I can imagine this might be complex if you are testing that two things are synced. But if you are not confident that a unit test assures that, then an Integration test is the way to go.

1

To reappropriate a quote from Jeff Goldblum in Jurassic Park; you were so busy making everything a service, that you failed to stop and think whether it needed to be a library.

Not everything should be a service. Nothing in this question leads me to believe that you need a separate runtime and app lifecycle.

I have multiple occasions where I want to trigger the three steps described above and I don’t want to duplicate the three steps

The need for reusability does not lead to the need for a service.

The alternative here is to use a library and refer to it during these “multiple occasions” that you talk about.

How you distribute this library is contextual. Maybe you’re working with a monorepo, maybe Nuget makes sense for your scenario, … Whatever fits the bill here.

We could Mock all three dependencies and make sure the right Message is passed to the Queue but that seems useless as we would reflect the implementation 1:1 in the test.

It sounds to me like that is precisely what your library’s purpose is. So if you were to write a test, yeah that’s how I’d go about it.

Maybe you think this is too trivial to test. While I generally raise the bar on what needs to be tested when something has public consumers, there’s still the reasonable cut-off that trivial code does not need to be tested. I’d err towards testing it but the opposite is arguable.

However, if you think it’s that trivial, maybe reconsider if it’s too trivial to warrant a library, let alone a microservice?

1

Is it ok to NOT test the sync service?

TDD, in its early forms, sends mixed messages here. At the time, people thought “never write a line of code without a failing test” was a good way to communicate what they were doing. But even in the original book (Test Driven Development by Example), Beck admitted that this wasn’t an absolute.

The heuristic I normally use:

  • Complicated code must be easy to test
  • Code that is hard to test must be “so simple there are obviously no deficiencies”

Therefore…

Is it ok to NOT test the sync service?

Yes, if it is simple enough.

Note that there is some tension here: you are probably going to end up judging this on whether it is simple enough today, but to some extent you also need practices that let you discover later that you left (some) code untested before the change that means its no longer “simple”.

When you have code that is both complicated and hard to test, refactor to separate the two. For example, if we look at your branching code alone, we might consider

public sync(Entity) 
{
    endPoints = this.EndpointRepository.getEndPoints(Entity)
    theLogic(Entity, endPoints)
}

theLogic(Entity, endPoints) {
    if ! endPoints.empty() {
        doSomethingUseful(Entity, endPoints)
    }
}

doSomethingUseful(Entity, endPoints) {
    msg = this.MsgFactory.create(Entity)
    this.MsgQueue.dispatch(msg, endPoints)
}

In my judgment, both sync and doSomethingUseful would meet the “so simple there are obviously no deficiencies” criteria.

I’d probably pass theLogic untested in a code review, but if you needed to have a test it might make sense to refactor a little further. One way to do that is to change from direct coupling to indirect coupling:

theLogic(Entity, endPoints) {
  theTestableLogic(Entity, endPoints, this.doSomethingUseful)
}

theTestableLogic(Entity, endPoints, theNextStep) {
   if ! endPoints.empty() {
      theNextStep(Entity, endPoints)
   }
}

For a toy problem, the ceremony dominates the useful work – there’s a reason that HelloWorld doesn’t have a lot of tests! But the ceremony grows much more slowly than complexity, so it can make sense to introduce seams when working in “real” code.

Given the answer of Ewan on the ‘how to test’: https://softwareengineering.stackexchange.com/a/446658/433888 I’ll try to reply to the ‘should we test it’ part of the question.

For me it is reasonable to test this class, especially when you start taking into account the unhappy scenario’s.
The intention of the method is to get data of an entity synchronized across different endpoints. The data from the repository and the endpoints could be on another machine.

With that in mind, what should happen in the method/class/system when any following occurs:

  • the repository is not available
  • the queue is not available
  • one or more endpoints are unavailable/returning (validation) errors/..

The tests should allow to simulate several of these scenario’s, and it could be done via mocking. These mocks could be setup to throw an exception in case that they are being called. Then will be about how and where you want to handle the problems and at what point you consider the data as correctly synchronized.

LEAVE A COMMENT