Question: Expecting a call to mock object from the constructor of the tested object?

196 views
Skip to first unread message

Chengwei Lin

unread,
Feb 22, 2022, 4:11:49 AM2/22/22
to Growing Object-Oriented Software
Hello everyone,

I'm at GOOS book chapter 15, in which I've come to a point where I want to write a unit test that expects a call to a mock object from within the constructor of the tested object.
Here is my unit test code, using JUnit 4.12 and jMock 2.6. (My code is slightly different from the original code in the book, and I've removed some parameters for clarity).
I want to expect that in AuctionSniper constructor, a call to sniperListener.sniperJoining() is made:

     1: @RunWith(JMock.class)
     2: public class AuctionSniperTest {
     3:     ...
     4:     private final SniperListener sniperListener = context.mock(SniperListener.class);
     5:     private final AuctionSniper sniper = new AuctionSniper(sniperListener);
     6:
     7:     @Test public void
     8:     reportsJoiningWhenConstructed() {
     9:             context.checking(new Expectations() {{
    10:                     one(sniperListener).sniperJoining();
    11:             }});
    12:        
    13:         // no calls to tested object
    14:     }
    15:     ...
    16: }

Because the call to sniperListener.sniperJoining() is expected to be made in AuctionSniper constructor (line 5), this unit test method does not call any AuctionSniper methods after the Expectations block (line 13).

When I run this unit test, I got the following JUnit failure:

    not all expectations were satisfied
    expectations:
      ! expected once, never invoked: sniperListener.sniperJoining()
    states:
      sniper has no current state
    what happened before this: nothing!
    ...

So I go to the constructor of AuctionSniper, and make the call there:

     1: public class AuctionSniper implements AuctionEventListener {
     2:     ...
     3:     private SniperListener sniperListener;
     4:
     5:            public AuctionSniper(SniperListener sniperListener) {
     6:                this.sniperListener = sniperListener;
     7:                sniperListener.sniperJoining();     // the call is made here
     8:            }
     9:     ...
    10: }

Then I run the unit test again and expect it to pass, but it does not:

    unexpected invocation: sniperListener.sniperJoining()
    no expectations specified: did you...
     - forget to start an expectation with a cardinality clause?
     - call a mocked method to specify the parameter of an expectation?
    what happened before this: nothing!
    ...

This failure message confuses me because in the previous failure it says sniperListener.sniperJoining() is "expected once, never invoked", but after I make the call it now says it is "unexpected invocation"? The other confusion is that this failure message says "no expectations specified", but I did specify the expectations in the unit test code (line 9 ~ 11)

So my questions are:

1. Does jMock allow us to expect a call to a mock object from within the constructor of the tested object?
2. If yes, what is the correct way to write such a unit test?

Your advice is appreciated.

Thanks,
Chengwei

Steve Freeman

unread,
Feb 22, 2022, 4:17:46 AM2/22/22
to Growing Object-Oriented Software
Because Java executes code from the top of the file downwards, you're calling your constructor before you've set up the expectation. So either you have to construct the object later (perhaps in the test itself) or you have to set up an expectation earlier (perhaps in a factory method when constructing the mock). Because these are fields, I don't belive that using a @Before method to setup the mock will work here.

jMock doesn't care where you call a mock, there's nothing special about a constructor.

Incidentally, this is one reason I prefer to keep my constructors very simple (i.e. just assign fields) and use factory methods for additional behaviour.

S

Chengwei Lin

unread,
Feb 28, 2022, 5:40:53 PM2/28/22
to Growing Object-Oriented Software
Hi Steve,

Based on your advice, the simplest solution seems to be moving the constructor into each test method, after the Expectations block:


     1: @RunWith(JMock.class)
     2: public class AuctionSniperTest {
     3:     ...
     4:     private final SniperListener sniperListener = context.mock(SniperListener.class);
     5:     // no constructor here

     6:
     7:     @Test public void
     8:     reportsJoiningWhenConstructed() {
     9:             context.checking(new Expectations() {{
    10:                     one(sniperListener).sniperJoining();
    11:             }});
    12:             new AuctionSniper(sniperListener);      // constructor here
    13:     }
    14:    
    15:     @Test public void
    16:     reportsLostIfAuctionClosesImmediately() {
    17:         context.checking(new Expectations() {{
    18:             allowing(sniperListener).sniperJoining();
    19:             one(sniperListener).sniperLost();
    20:         }});
    21:         AuctionSniper sniper = new AuctionSniper(sniperListener);   // constructor here
    22:         sniper.auctionClosed();
    23:     }
    24:     ...
    25: }

After doing so, the tests are running as expected.
And I also found that this seems similar to the NUnit style, where the same instance of the test class is reused for all the test methods, so the constructor cannot be put at the class field, either.
Anyway, thank you very much for your helpful advice!

Regards,
Chengwei

Steve Freeman 在 2022年2月22日 星期二下午5:17:46 [UTC+8] 的信中寫道:

Steve Freeman

unread,
Feb 28, 2022, 5:44:28 PM2/28/22
to Growing Object-Oriented Software
Good to see that this works. I think it's a hint to think about your design and whether you want significant behaviour in your constructor.

I remember making the case at the time that NUnit got it wrong -- but too late for history. The JUnit style, although unusual, is good for forcing a refresh.

Incidentally, you might also want to take a look at using the jmock rule for junit (don't have the syntax handy).

S
Reply all
Reply to author
Forward
0 new messages