1

I've recently been working on a customerproject that makes extensive use of a signals/slots like mechanism. Their approach is to directly connect signals from a larger framework to private methods of their businessobjects. Now, my role has mostly to do with unittesting, and their approach makes testing fantastically hard, since most of the stimulation received by the businessobjects is generated by the framework by means of the signals, which leaves my unable to touch the actual logic of the businessobjects (their public interface is super anemic, often only consisting of a C'tor/D'tor), without having a complete running instance of the framework between the test and the code under test, i.e. I basically end up with an integration test.

I feel slots used that way should actually be part of a class' public interface, since they are called by external code, but I'm having a hard time finding better better arguments for this "feeling" other than "It makes testing easier". If the design were mine I'd mandate to only ever use public methods for callbacks/slots, but since that's customercode I can't make that decision, but need to convince the customer's engineers to do that, especially if they argue, that what they're doing, is actually "encapsulation".

So far my list of points are:

  • Using private methods this way makes the interface of the class "lie", i.e.: In the headerfile the method is marked as private, while it is actually sort of public for a specific other object.
  • From a practical standpoint it makes unit testing hard or even impossible.

My suggestions would be:

  • Either don't use that signals/slots like mechanism altogether (which is probably not feasible)
  • Or: Only use public methods of a businessobject as slot, ideally coupled with separating the "wiring" of the signals from their definition, so that a testenv can wire up the signals differently or even call the respective methods directly.

Am I missing something? Am I completely wrong with the opinion, that slots are actually part of the public interface and should therefore not be hidden away?

Edit: To clarify a bit more. The class hierarchy I have to deal with is this (simplified!)

ObjectContainingSignals <--- Inherit --- BusinessObject

And the border between the framework and the actual application is basically the inheritance here, so what happens is, that the applicationcode usually does something like:

this.connect(this->privateHandlerFunc, SomeSignalName)

@Sebastian Redls suggestion would be great in cases where instead of inheritance we have something like this:

ObjectContainingSignals <--- use --- BusinessObject

and we have the option inject ObjectContainingSignals into the BO (ideally ObjectContainingSignals would be hidden behind an interface).

rincewound
  • 129
  • 4

1 Answers1

3

Where do the signals come from? Are they signals on objects passed to the constructor?

If so, are these objects mockable? Then you have your answer. Create mocks of the dependencies, pass them to the constructor, let it hook its slots to the signals, then trigger the signals on your mocks to achieve your testing.

If the constructors instead access global state, then that's an issue for testing anyway.

But the more general issue is one of responsibilities. Is it really the responsibility of the object having the slots to hook them up to signals? The answer to that question is probably the same as "Is the object useful without being hooked up?" If a component receives signals from its own children, the slots should probably not be externally accessible. If the signals come from anywhere else, I'd argue that it's probably a mistake if the component connects itself.

Sebastian Redl
  • 14,950
  • 7
  • 54
  • 51
  • I guess the main issue here is, that the object attempts to wire up the signals itself, that is a very good point. If that weren't the case testing wouldn't make any problems at all, however: If the object would leave wiring signals to some other entity, the corresponding functions would have to be public. – rincewound Mar 22 '21 at 10:12