Search code examples
mongodbinversion-of-controlfantom

How to unit-test a Fantom DAO class that uses AlienFactory's afIoc and afMorphia frameworks?


Say I have a simple Fantom app to retrieve data from a MongoDB database. The app uses afIoc (AlienFactory's IoC framework) and afMorphia (AlienFactory's MongoDB framework). I have a simple Entity called Foo and a DAO class (FooDao) to retrieve Foo entities from the database. I want to test the DAO class using fant command but I'm getting the error below. What am I doing wrong or is there a better way to write the test?. Please note that this app doesn't use afBedSheet web framework.

TEST FAILED
afIoc::IocErr: No dependency matches type sys::Type.
Ioc Operation Trace:
  [ 1] Injecting dependencies into fields of testAfIoc::FooDaoTest
  [ 2] Injecting dependencies into fields of testAfIoc::FooDaoTest
  [ 3] Looking for dependency of type testAfIoc::FooDao?
  [ 4] Creating REAL Service 'testAfIoc::FooDao'
  [ 5] Creating 'testAfIoc::FooDao' via ctor autobuild
  [ 6] Injecting dependencies into fields of testAfIoc::FooDao
  [ 7] Looking for dependency of type afMorphia::Datastore?
  [ 8] Creating REAL Service 'afMorphia::Datastore'
  [ 9] Creating 'afMorphia::Datastore' via ctor autobuild
  [10] Determining injection parameters for afMorphia::DatastoreImpl sys::Void make(sys::Type type, afMongo::Database database, |sys::This->sys::Void| in)
Stack Trace:
  afIoc::Utils.stackTraceFilter (Utils.fan:50)
  afIoc::RegistryImpl.injectIntoFields (RegistryImpl.fan:253)
  testAfIoc::FooDaoTest.setup (FooDaoTest.fan:17)
  java.lang.reflect.Method.invoke (Method.java:483)
  fan.sys.Method.invoke (Method.java:559)
  fan.sys.Method$MethodFunc.callList (Method.java:204)
  fan.sys.Method.callList (Method.java:138)
  fanx.tools.Fant.runTest (Fant.java:190)
  fanx.tools.Fant.test (Fant.java:110)
  fanx.tools.Fant.test (Fant.java:32)
  fanx.tools.Fant.run (Fant.java:284)
  fanx.tools.Fant.main (Fant.java:327)

Time: 377ms

Failed:
  testAfIoc::FooDaoTest.testFindAll

***
*** 1 FAILURES [1 tests, 0 methods, 0 verifies]
***

This is how my classes look like (explicit "using" statements to show what classes come from what Fantom pods):

fan/AppModule.fan

using afIoc::Configuration
using afIoc::Contribute
using afIoc::ServiceDefinitions
using afIocConfig::ApplicationDefaults
using afMorphia::MorphiaConfigIds
using afMorphia::Datastore

class AppModule {

  @Contribute { serviceType=ApplicationDefaults# }
  static Void contributeAppDefaults(Configuration config) {
    config[MorphiaConfigIds.mongoUrl] = `mongodb://localhost:27017/foo`
  }

  static Void defineServices(ServiceDefinitions defs) {
    defs.add(FooDao#)
    defs.add(Datastore#)
  }

}

fan/Foo.fan

using afMorphia::Entity
using afMorphia::Property
using afBson::ObjectId

@Serializable
@Entity { name="foo" }
class Foo {

  @Property const ObjectId _id
  @Property const Str text

  new make(|This| f) { f(this) }
}

fan/FooDao.fan

using afIoc::Inject
using afMorphia::Datastore

class FooDao {

  @Inject { type=Foo# }
  Datastore? datastore

  Foo[] all() {
    (Foo[]) datastore.findAll
  }
}

fan/FooDaoTest.fan

using afIoc::Inject
using afIoc::RegistryBuilder
using afIocConfig::ConfigModule
using afMorphia::Datastore

class FooDaoTest : Test {

  @Inject
  FooDao? subject

  override Void setup() {
    registry := RegistryBuilder().addModules([
      AppModule#
      ,ConfigModule#
      ,Datastore#
    ]).build.startup
    registry.injectIntoFields(this)
  }

  Void testFindAll() {
    verifyEq ( subject.all.size, 2 )
  }
}

build.fan

#! /usr/bin/env fan

using build

class Build : BuildPod {

  new make() {
    podName = "testAfIoc"
    summary = "Testing AF IoC"
    version = Version("0.0.1")
    depends = [
      "sys 1.0+",
      "afIoc 2.0.2+",
      "afIocConfig 1.0.16+",
      "util 1.0+",
      "afMongo 1.0.0+",
      "afBson 1.0.0+",
      "concurrent 1.0.8+",
      "afMorphia 1.0.2+"
    ]
    srcDirs = [
      `fan/`,
    ]
    meta = [
      "proj.name" : "Testing Alien Factory IoC framework",
      "afIoc.module" : "testAfIoc::AppModule",
      "org.name" : "Foo",
      "org.uri"  : "http://www.example.com/"
    ]
  }
}

Solution

  • Everything is setup right, and the way you're testing the DAO is spot on. (Morphia doesn't need Foo to be @Serializable by the way...)

    The problem is where you build the Registry. Because Morphia is an IoC library, you need to add Morphia's module to the registry. Also note that Datastore is not a module and should not be added:

    In fan/FooDaoTest.fan

    using afIoc::Inject
    using afIoc::RegistryBuilder
    using afIocConfig::ConfigModule
    using afMorphia::MorphiaModule
    
    class FooDaoTest : Test {
    
      @Inject
      FooDao? subject
    
      override Void setup() {
        registry := RegistryBuilder()
          .addModule(AppModule#)
          .addModulesFromPod("afMorphia")
          .addModulesFromPod("afIocConfig")
          .build.startup
        registry.injectIntoFields(this)
      }
    
      Void testFindAll() {
        verifyEq ( subject.all.size, 2 )
      }
    }
    

    When building your own Registry, make sure all the relevant IoC modules and libraries are added.

    I'm surprised that the code didn't throw a 'Datastore' Service Not Found Err (which is more telling) - I'll look in to that...

    Edit:

    Ah, yes. In your AppModule there's no need to add Datastore as a service. All libraries should define their own services in their module, so all you should ever need to do is add that module (and maybe configure some services).

    (Morphia has a special dependency provider for building Datastore instances which is why it didn't work when you added it as a plain service.)

    Removing the Datastore definition from AppModule and re-running the broken test gives:

    TEST FAILED
    afIoc::IocErr: No service matches type afMorphia::Datastore?.
    

    which is (a bit) more informative and what I would expect.