How to bind a spy to a constant instance

674 views
Skip to first unread message

Jared Martin

unread,
Jun 18, 2013, 9:04:37 AM6/18/13
to juk...@googlegroups.com
I have a class that I use often in my code, for SSCCE reasons call it Context The class is immutable, so I can't give it a no-arg constructor. It's mostly a data object, but it has some convenience stuff. For example, I also often use this class as the Key in Maps. It's also often used as an argument to AssistedInject factories to make sure my new Object is using the same Context as the object that created it.

Anyway, in my most recent unit test, I ended up doing something like:


    @RunWith(JukitoRunner.class)
    public class TestCase {
        private static final Context c = new Context("foo", "bar", "baz");
        public static class TestModule extends JukitoModule {
            @Override
            protected void configureTest() {
                // some other stuff
            }
        }
       
        private Context spyContext;
        private ClassUnderTest cut;

        @Before
        public void setupMocks(ClassUnderTestFactory fact) {
            spyContext = spy(c);
            cut = fact.create(spyContext);
        }
    }

I would much prefer to just do:


    @RunWith(JukitoRunner.class)
    public class TestCase {
        public static class TestModule extends JukitoModule {
            @Override
            protected void configureTest() {
                bindSpy(Context.class).toInstance(new Context("foo", "bar", "baz"));
            }
        }
       
        private ClassUnderTest cut;

        @Before
        public void setupMocks(Context spyContext) {
            cut = fact.create(spyContext);
        }
    }

Can this be done already? If not, what do you think of this feature?

Stephan Classen

unread,
Jun 18, 2013, 9:13:25 AM6/18/13
to juk...@googlegroups.com
Did you try to run you second code snippet ??
--
You received this message because you are subscribed to the Google Groups "Jukito" group.
To unsubscribe from this group and stop receiving emails from it, send an email to jukito+un...@googlegroups.com.
For more options, visit https://groups.google.com/groups/opt_out.
 
 

Philippe Beaudoin

unread,
Jun 18, 2013, 9:41:14 AM6/18/13
to juk...@googlegroups.com
bindSpy is a bit tricky, using an internal relaying key to bind the spy to the constructor injection point of the provided class.  (Look at bindNewSpyProvider(Key<T> key) in TestModule)

Have you tried:

bind(Context.class).toInstance(spy(new Context("foo", "bar", "baz")));

I believe this would do what you're looking for.

If you want to investigate your idea further, though, the problem I see is that your proposed syntax would not be backwards compatible. (bindSpy would have to return an intermediate interface WITHOUT performing the actual bind, whereas it is currently expected to create the binding.) a possible option would be to overload the method:

bindSpy(Context.class, new Context("foo", "bar", "baz"));

(and all 4 variants)

Slightly more opaque, but at least you preserve backwards compatibility.

Another option might be to try to extend the Guice DSL and move Jukito in that direction:
  bind(Context.class).toSpy();
  bind(Context.class).toSpyInstance(new Context("foo", "bar", "baz"));
  bind(Context.class).toMock();

I'm really not sure the DSL can be extended this way though. Sounds like a challenge. But anything in that direction could be interesting.

Cheers,

    Philippe




Jared Martin

unread,
Jun 18, 2013, 2:04:21 PM6/18/13
to juk...@googlegroups.com
On Tuesday, June 18, 2013 8:41:14 AM UTC-5, Philippe Beaudoin wrote:
Have you tried:

bind(Context.class).toInstance(spy(new Context("foo", "bar", "baz")));

I believe this would do what you're looking for.

 This worked perfectly!

If you want to investigate your idea further, though, the problem I see is that your proposed syntax would not be backwards compatible. (bindSpy would have to return an intermediate interface WITHOUT performing the actual bind, whereas it is currently expected to create the binding.) a possible option would be to overload the method:
 
Rest of your reply snipped. I agree with everything you've said and think that since the proposed functionality is available as you've described, I don't think it's necessary. However, I suspect that the scope of your answer is effectively .in(Singleton.class) and not .in(TestSingleton.class). I don't know if that would cause any issues, but I didn't see any in my refactored test case. Though if you do manage to extend the DSL, I'd be happy to take a look :)

Thanks!


-Jared

Philippe Beaudoin

unread,
Jun 18, 2013, 2:23:43 PM6/18/13
to juk...@googlegroups.com
A toInstance bind will necessarily be in singleton (the object is instantiated once at bind-time). If your spied Context is immutable it's fine. For a mutable object, however, you have to bind it in TestSingleton (otherwise you'll get cross-talk between your tests) and you'll need a @Provides method that return the spy.


--

Jared Martin

unread,
Jun 18, 2013, 4:03:26 PM6/18/13
to juk...@googlegroups.com
Ah crap! Here's a quick test:

import org.jukito.JukitoModule;
import org.jukito.JukitoRunner;
import org.junit.Test;
import org.junit.runner.RunWith;

import static org.mockito.Mockito.*;

@RunWith(JukitoRunner.class)
public class JukitoTest {

    public static class TestModule extends JukitoModule {       
        @Override
        protected void configureTest() {
            bind(Context.class).toInstance(spy(new Context("hello world")));
        }
    }
   
    public static class Context {
        private final String val;
        public Context(String val) {
            this.val = val;
        }
        public String getVal() {
            return val;
        }
    }
   
    @Test
    public void testJukitoOne(Context cxt) {
        String foo = cxt.getVal();
       
        verify(cxt).getVal();
    }
   
    @Test
    public void testJukitoTwo(Context cxt) {
        verify(cxt, never()).getVal();
    }
}


I guess that's not going to work! Let me try your provider way...

Philippe Beaudoin

unread,
Jun 18, 2013, 4:07:02 PM6/18/13
to juk...@googlegroups.com
Ah! You're right. I forgot about the fact that the spy-wrapped object WAS mutable. Yeah, you'll need a @Provides method. (And the 2-param bindSpy becomes much more interesting.)

Jared Martin

unread,
Jun 18, 2013, 4:13:20 PM6/18/13
to juk...@googlegroups.com
Another (faster) way would be to simply add a Generic provider to the package, it's a very simple class. Here's the class (as a static inner class) and a test to verify that it works...

import org.jukito.JukitoModule;
import org.jukito.JukitoRunner;
import org.jukito.TestSingleton;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;

import com.google.inject.Provider;


import static org.mockito.Mockito.*;

@RunWith(JukitoRunner.class)
public class JukitoTest {
    public static class TestModule extends JukitoModule {       
        @Override
        protected void configureTest() {
            bind(Context.class).toProvider(
                    new SpyInstanceProvider<Context>(new Context("hello world")))
                    .in(TestSingleton.class);
        }
    }
   
    public static class SpyInstanceProvider<T> implements Provider<T> {
        private final T instance;
       
        public SpyInstanceProvider(T instance) {
            this.instance = instance;
        }
       
        @Override
        public T get() {
            return spy(instance);

        }
    }
   
    public static class Context {
        private final String val;
        public Context(String val) {
            this.val = val;
        }
        public String getVal() {
            return val;
        }
    }
   
    @Before
    public void setUp(Context c) {
        c.getVal();
    }
   
    @Test
    public void testJukitoTwice(Context sec) {
        String foo = sec.getVal();
       
        verify(sec, times(2)).getVal();
    }
   
    @Test
    public void testJukitoOnce(Context sec) {
        verify(sec).getVal();
    }
}


What do you prefer, this version or a 2-arg bindSpy?

-Jared

Jared Martin

unread,
Jun 18, 2013, 4:23:56 PM6/18/13
to juk...@googlegroups.com
In fact, you could even do both, and just make the new method calls just be a wrapper for the provider, i.e. , in TestModule:

        protected <T> ScopedBindingBuilder bindSpy(Class<T> klass, T instance) {
            return bind(klass).toProvider(new SpyInstanceProvider<T>(instance));
        }


Maybe I'll submit a pull request :)

-Jared


You received this message because you are subscribed to a topic in the Google Groups "Jukito" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/jukito/LkJBQRZA6qw/unsubscribe.
To unsubscribe from this group and all its topics, send an email to jukito+un...@googlegroups.com.

Philippe Beaudoin

unread,
Jun 18, 2013, 5:16:22 PM6/18/13
to juk...@googlegroups.com
I like this. Make it a separate file, though.

Stephan Classen

unread,
Jun 19, 2013, 6:51:30 AM6/19/13
to juk...@googlegroups.com
I think this is still kind of hazardous.
Because the underlaying instance will be the same for every test method. only the spy itself will be a new instance.
So if i call a method on the spy in test1. This may change the state of the underlaying instance.
When test2 now runs it sees the changes made from test1.

Jukito has already a class SpyProvider<T> this class uses another provider to get a new instance for the spy for every test method.
This way it is save to call methods on the spy which change the state of the underlaying instance.

Philippe Beaudoin

unread,
Jun 19, 2013, 9:38:08 AM6/19/13
to juk...@googlegroups.com
You're absolutely right, Stephan, adding a method is not a good idea... Even a SpyInstanceProvider<> is tricky as it makes it quite easy to shoot yourself in the foot if the underlying object is mutable. The real problem, though, is that bindSpy only looks for constructor injection point. If you happen to have a @Provides method for the object you're spying, you're out of luck.

I'd like you to be able to do something like:


@RunWith(JukitoRunner.class)
public class JukitoTest {

    public static class TestModule extends JukitoModule {        
        @Override
        protected void configureTest() {
            bindSpy(Context.class);
    }
    
    @Provides
    Context getContext() {
        return new Context("hello world");
    }
}

But currently I believe this will not work... It should be relatively easy to fix though. (ie: bindSpy should first look for a binding, if not found then look for a constructor injection point.)

Jared Martin

unread,
Jun 19, 2013, 10:16:58 AM6/19/13
to juk...@googlegroups.com
I agree that it's hazardous but that's up to the person writing the test. In the pull request I'm making I'm writing the documentation to reflect this.

Stephan, the problem is, you can't supply Jukito with your own provider for use with SpyProvider. (since JukitoInternal isn't public). Otherwise I would agree with you, you could just use that provider.... which is what Philippe essentially just said as I was finishing my test class for the pull request.

Anyway, I finished it (github username durron597), feel free to use it or do it this other way.

-Jared


You received this message because you are subscribed to a topic in the Google Groups "Jukito" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/jukito/LkJBQRZA6qw/unsubscribe.
To unsubscribe from this group and all its topics, send an email to jukito+un...@googlegroups.com.
Reply all
Reply to author
Forward
0 new messages