Lee Jaedong
Lee Jaedong

Reputation: 95

How to test a public method that changes a variable with defined getter

I have a game that has coins. There is an Add() method that changes the coins. I want to test if it adds correctly.

public class CoinsService 
{
    public ReactiveProperty<long> Coins { get { return State.SaveGame.Coins; } }

    public int Add(int coins)
    {
         Coins.Value += coins;
         return coins;
    }
}

The test:

public class CoinServiceTests
{
     [Test]
     public void AddCoins_WhenCalled_AddsUpToTotalCoins()
     {
          var coinsService = new CoinsService();
          coinsService.Add(10);
          Assert.That(coinsService.Coins.Value, Is.EqualTo(10));
     }
}

I have tried making a substitute of the class using NSubstitute like that:

var coinsService = Substitute.For<CoinsService>();

and instantiating a fresh instance of the Coins like that

coinsService.Coins.Returns(new ReactiveProperty<long>());

also like that

var coins = new ReactiveProperty<long>();
coinsService.Coins.Returns(coins);

I expect that that after doing any of the above, I will be able to check the Coins value. Instead I'm getting a null reference object exception that the coinsService.Coins object is null

To clarify, the null reference appears on the line

public ReactiveProperty<long> Coins { get { return State.SaveGame.Coins; } }

Upvotes: 0

Views: 739

Answers (2)

Louis Go
Louis Go

Reputation: 2588

Don't mock any object shall be tested. Mocked instance is used for something "I tested it, but I need a fake one for another test"

In your case, what State.SaveGame.Coins shall be mocked but not CoinsService, because CoinsService is going to be tested.

To make CoinsService testable, most common way is refactor CoinsService to accept Dependency Injection container and get rid of static State.

// An interface to provide Coins
    public interface ICoinProvider
    {
        ReactiveProperty<long> Coins{ get; }
    }

    public class CoinsService{

        // Ctor take the instance in it
        public CoinsService( ICoinProvider provider )
        {
            _CoinProvider = provider;
        }

        private ICoinProvider _CoinProvider;

        public CoinWrap Coins
        {
            get
            {
                return _CoinProvider.Coins;
            }
        }

        public int Add( int coins )
        {
            Coins.Value += coins;
            return coins;
        }
    }

Then mock ICoinProvider

     [Test]
     public void AddCoins_WhenCalled_AddsUpToTotalCoins()
     {
          var mock = Substitute.For<ICoinProvider>();
          mock.Coins.Returns( new ReactiveProperty<long>( 10 ) );
          var coinsService = new CoinsService( mock );
          coinsService.Add(10);
          Assert.That(coinsService.Coins.Value, Is.EqualTo(10));
     }

If you found this refactor is hard to be done, then you may read Charlie's post.

Upvotes: 1

Charlie
Charlie

Reputation: 13736

You are trying to test CoinsService because it does the addition. Therefore, you have to use a real CoinsService rather than mocking it. Mocking is for classes that collaborate with the class you are trying to test.

Looking at your code, I see why you thought this should work... you have this line of code...

coinsService.Coins.Returns(new ReactiveProperty<long>());

This causes a new Coins property to be created each time it is accessed, which is why you have the error you are seeing.

I suspect that the root cause of this is that CoinsService is a large class, with lots of functionality, which you don't want to instantiate just to test the ability to add coints. That leads to wanting to mock it. (If that's not true, we can stop here - just don't mock it!)

If CoinsService is "too big to test", then it needs to be broken down in such a way that it uses collaborators. For example, imagine that Coins were a class than just a long. It could have an Add method... Add(long howmuch) for example.

Then CoinsService would be changed just a bit to do...

public int Add(int coins)
{
     Coins.Add(coins);
     return Coins.Value; // I believe your original return is in error
}

Now this makes it all a bit more indirect, but gives you the advantage that you can test the addition function by testing the Coins class, without using a service.

You can (and should) also test the service itself by creating a mock for Coins and ensuring that its Add method is called when CoinService.Add is called.

Upvotes: 3

Related Questions