简体   繁体   中英

Grails: How to mock properties for domain object with non-domain abstract base class

I have a grails 2.2.4 app with a domain class Monster :

class Monster {
    int aggression
}

I can mock and test it like so:

import spock.lang.*
class MonsterSpec extends Specification {
    def "property mocks work"() {
        given:
        def m = Mock(Monster)
        m.aggression >> 5

        expect:
        m.aggression == 10
    }
}

Recently I decided to give it an abstract base class ( not a domain object itself) so that I could share method implementations among my many Monster -like classes:

abstract class Entity {} // Not under /domain

class RefactoredMonster extends Entity {
    int aggression
}

But then a thousand simple tests all broke, like so:

import spock.lang.*
class MonsterSpec extends Specification {
    def "property mocks work"() {
        given:
        def m = Mock(RefactoredMonster)
        m.aggression >> 10

        expect:
        m.getAggression() == 10 // This works

        and:
        m.aggression == 10 // This fails!  m.aggression is null!
    }
}

What on Earth is going on? The problem goes away if I make Entity concrete, but then of course I can't hydrate any Monster objects since Hibernate doesn't know what to do with Entity (and I don't want to make Entity a domain object, though I suppose I will if I really must).

What am I missing?

The problem is GORM expects the super class to be a domain class.

With Groovy 2.0, which is what Grails 2.2.4 has, you can use compile-time mixins to add methods to a class. This allows for method reuse without inheritance.

The Entity can remain as not a domain class, but it must be a concrete class. Then, instead of subclassing, use it as a mixin.

@Mixin(Entity)
class RefactoredMonster {
    int aggression
}

An alternative

Since you need the ability to override methods, as you said, Mixins are out.

Looking at this from a higher level, a potential issue is the architecture/design. Inheritance is meant to represent is-a relationships (ex. a Dog is an Animal). But when inheritance is used primarily as a way to reuse methods, it can lead to... a mess.

It may be better to forgo inheritance and choose has-a (delegation) instead. This would allow you to reuse behaviour and override it when needed. Unfortunately, Groovy 2.0 doesn't support @Delegate. So the following example is going to have more boilerplate code than the same thing coded in Groovy 2.4 would:

interface Flier {
    def fly();
}

class FlierImp {
    def fly() { "I'm fying! WOOT!" }
}

class RealDuck implements Flier {
    def flier

    RealDuck() {
        flier = new FlierImp() // Purposely not using injection
    }

    def fly() {
        flier.fly()
    }
}

class RubberDuck implements Flier {
    def fly() { "I don't fly" }
}

def duck = new RealDuck()
def rubberDuck = new RubberDuck()

assert duck.fly() == "I'm fying! WOOT!"
assert rubberDuck.fly() == "I don't fly"

In the example above RealDuck and RubberDuck represent domain classes (which is why I'm not injecting the flier). The flying behaviour is demanded by an interface and implemented either via a class which only implements the behaviour ( FlierImp ), or by implementing it directly, as shown in RubberDuck .

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM