Spring4D: best strategy for unit testing with Spring.Collections 2.0

173 Views Asked by At

Once we migrate from 1.2.6 to 2.0 there is no more RTTI for collection interfaces (IList<>, IDictionary<> etc.). We had a lot of tests which relied on dynamic mocking behaviour where just mocked collection was returned and that was enough for the test purpose. But now this is not possible to leave as is as all call return nil instead of auto-mock (as there is no RTTI).

IService1 = interface(IInvokable)
 [...]
 function Errors: IList<TError>;
end

So now we always need to setup mocks when code accesses the Error method. The bigger problem is when the interface descends from SpringCollections interface like this:

IServices2 = interface(IList<TRecord>)
  [...]
  procedure SomeOtherMethod;
end;

where setup of mock cannot be done for collection methods (from the ascendant interface).

  • It's only in about 5 cases where IList/IDictionary are inherited and TList/TDictionary<k,v> classes are used as a base class. In those cases, they are small extensions of the collections functions;
  • The main problem is that when a certain service have methods that return for example IList, IDictionary<Integer, TRecord>. Then since Spring 2.0 when testing a class that uses such a service it is obligatory to do Mock.Setup.Returns(TCollections.CreateList).When.GetRecords - and before Mock automatically returned a mocked instance;

So now whenever there are methods returning Spring.Collections.I* in the injected dependency we always need to setup them in all tests which is the same as using TMockBehavior.Strict (ask why dynamic is default - maybe because people do not want to always setup mocks for all methods invoked in the tested code).

For now I migrated almost all the codebase, but the last issue is still present in few test cases and it's hard verify the call to service based on the spring collections.

What do you suggest as best strategy for future unit tests?

  1. Just bite the bullet and add those extra lines every time even when you do not need that;
  2. Enable RTTI (also good question how?) for Spring.Collections in test projects (some define for a compilator - that is not a problem technically).
1

There are 1 best solutions below

1
On

The behavior in 1.2 might have been convenient and might have worked in many cases but was not a good behavior.

Let me explain why I think so: What it did was return a mocked collection which did nothing but accept any call and return default values for any type. And that could result in calls where you would first call Add and then try to get back the item but it would give you nothing. It would not even cause an out-of-range exception as if it were an empty collection. It would just give you a nil customer or 0 or empty string or whatever the default of the data type is that you have in your list.

When mocking in dynamic mode this is usually done when you have some services as dependencies but don't care if they do anything or what they return. However, when some service returns a collection this often is not the case. You either expect the collection to contain some data that you process further or if the method serves as a getter you expect that the data you put into the collection is still in there the next time you call that function.

That means there are two different common patterns for dealing with collections in mocks (which depend on the implementation or rather API specification of your mocked interface):

Returning a new instance of a collection on every call - then you have to specify like this (ignoring the correct settings for ownsObjects for this demonstration):

var
  mock: Mock<IService>;
begin
  mock.Setup.Executes(
    function: TValue 
    begin 
      Result := TValue.From(TCollections.CreateList<TError>) 
    end).When.Errors;

If the method serves as a getter you have to specify this:

var
  mock: Mock<IService1>;
begin
  mock.Setup.Returns(TCollections.CreateList<TError>).When.Errors;

Which one you pick really depends on how IService1 is supposed to work but simply returning a dynamic mock of IList<TError> that does nothing hardly leads to robust in meaningful tests.