0

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
  • The first and obvious question: why do you have a service that only connects to other services? I'd be looking to remove it entirely, rather than test it. Jul 20, 2023 at 13:50
  • 2
    You seem to have the unfortunately common disease of "microservice-itis" which has symptoms of any common code being put into a separate service, despite the fact that this is in most cases massive overengineering. Jul 20, 2023 at 13:56
  • 1
    When you say SyncService::syncFoo do you mean SyncService::sync(foo)? Jul 20, 2023 at 14:17
  • 3
    I agree with @PhilipKendall that this service shouldn’t exist if you own all other services involved. Consider PubSub: services emit events that other services can subscribe to (event driven architecture).
    – Rik D
    Jul 20, 2023 at 15:23
  • 2
    I'm always stunned to see a valid question with comments that challenge the question without providing a solution, and without accepting that there can be very valid reasons to do so. Software engineering should not be driven by prejudice imho.
    – Christophe
    Jul 21, 2023 at 6:42

4 Answers 4

4

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
  • The main advantage I see in the mocking approach is that you can test when the other services are down. That said, I'd prefer the integration test in this case, especially if there's something about the syncing of data that you want to test (i.e. If a date value changed in one service is reflected in the synced service - you'd want a test-ready item that you can change the date in Service Y, and see that it reflects properly in Service X - without mocking how you think the Service Y translates that value [In particular, around DST/timezone differences.] to Service X through an API.). Aug 22, 2023 at 20:52
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
  • hi I don't know why you and others in the comments think its about microservices, its about a service class I never mentioned microservices
    – gries
    Jul 24, 2023 at 9:09
0

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.

0

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.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.