vendredi 28 octobre 2016

DI for strategy class to make it testable

The name of thread is a little bit strange, but let me explain what problem I have. First of all let's say, that I have MVP architecture and I'm using injections. Also I have some service, which implements strategy pattern. Let's see how it looks like:

public class SettingsService<T> where T : Setting
{
    private ISettingsParser<T> settingsParser;

    public SettingsService(ISettingsParser<T> settingParser)
    {
        this.settingsParser = settingParser;
    }

    public void SaveSetting(T setting)
    {
        settingsParser.SaveSettings(setting);
    }

    public T GetSetting()
    {
        return settingsParser.GetSettings();
    }
}

To use this service you should pass as parameter new object, which is the concrete implementation of any parser, which actually knows how to parse file with concrete settings.

Here is example how we can create settings service to parse Player panel settings:

            var settingsService = new SettingsService<PlayerPanelSettings>(
            new PlayerPanelSettingsParser(fileWorker));

Don't pay attenetion on fileWorker, it's just a wrapper which allows you working with files and any type of parser gets it as parameter.

Now let's talk about presenters. For example we have presenter for Player panel settings which looks like:

public partial class PlayerPanelSettingsPresenter :
         BasePresenter<IPlayerPanelSettingsView, PlayerPanelSettings>
{
    private PlayerPanelSettings playerPanelSettings;

    private IMessageParser messageParser;

    private IFileWorker fileWorker;

    private SettingsService<PlayerPanelSettings> settingsService;

    public PlayerPanelSettingsPresenter(
            IApplicationController controller,
            IPlayerPanelSettingsView playerPanelSettingsView,
            IMessageParser messageParser,
            IFileWorker fileWorker) :
            base(controller, playerPanelSettingsView)
    {
        this.messageParser = messageParser;
        this.fileWorker = fileWorker;
        settingsService = new SettingsService<PlayerPanelSettings>(
            new PlayerPanelSettingsParser(fileWorker));
        SubscribePresenters();
    }

    private void SubscribePresenters()
    {
        View.ChangePlayerPanelSettings += ChangePlayerPanelSettings;
    }

    public void ChangePlayerPanelSettings(
                    PlayerPanelSettings newSettings)
    {
        if (View.ValidateForm())
        {
            SaveSettings(newSettings);
            playerPanelSettings.SetSettings(newSettings);
            View.Close();
        }
        else
        {
            ChallengeMessage message =
                messageParser.GetMessage(MessageType.FtpSettingsInvalid);
            View.ShowMessage(message);
        }
    }
    private void SetPlayerPanelSettings()
    {
        View.SetPlayerPanelSettings(playerPanelSettings);
    }
    private void SaveSettings(PlayerPanelSettings newSettings)
    {
        settingsService.SaveSetting(newSettings);
    }
}

It works pretty simple - we determines event handler for event which we will recieve from our View. Also in constructor of presenter we extend base with services, which our concrete presenter need. In this case it's MessageParser service, FileWorker service and concrete SettingsService (for player panel settings in this case). And as you can see we are using dependency inversion principle for Message parser and File worker. Now in our dependency resolver we should bind abstraction with concrete impelentation, like RegisterService<IMessageParser, MessageParser>().

And let's look how we can test ChangePlayerPanelSettings method in our presenter (I'm using NUnit and NSubstitute):

[TestFixture]
class PlayerPanelSettingsTest : TestCase
{
    private IApplicationController controller;
    private PlayerPanelSettingsPresenter presenter;
    private IPlayerPanelSettingsView view;
    private PlayerPanelSettings mock;
    private PlayerPanelSettings argument;
    private SettingsService<PlayerPanelSettings> settingService;
    private IMessageParser messageParser;

    [SetUp]
    public void SetUp()
    {
        controller = Substitute.For<IApplicationController>();
        view = Substitute.For<IPlayerPanelSettingsView>();
        messageParser = Substitute.For<IMessageParser>();
        IFileWorker fileWorker = Substitute.For<IFileWorker>();
        PlayerPanelSettingsParser parser =
            Substitute.For<PlayerPanelSettingsParser>(fileWorker);
        settingService =
            Substitute.For<SettingsService<PlayerPanelSettings>>(parser);
        presenter = new PlayerPanelSettingsPresenter(
            controller, view, messageParser, fileWorker);
        mock = Substitute.For<PlayerPanelSettings>();
        argument = InitializePlayerPanelSettings();
        presenter.Run(mock);
    }
    [Test]
    public void ChangePlayerPanelSettingsIfFormIsValid()
    {
        // Arrange
        SetFormAsValid(true);
        var returnedMessage = new ChallengeMessage()
        {
            MessageType = MessageType.PlayerPanelSettingsInvalid
        };
        // Act
        presenter.ChangePlayerPanelSettings(argument);
        // Assert
        settingService.DidNotReceive().SaveSetting(argument);
        mock.Received().SetSettings(argument);
        view.Received().Close();
        messageParser.DidNotReceiveWithAnyArgs().GetMessage(MessageType.PlayerPanelSettingsInvalid);
        view.DidNotReceiveWithAnyArgs().ShowMessage(returnedMessage);
    }


    [Test]
    public void ChangePlayerPanelSettingsIfFormIsInvalid()
    {
        // Arrange
        SetFormAsValid(false);
        var returnedMessage = new ChallengeMessage()
        {
            MessageType = MessageType.PlayerPanelSettingsInvalid
        };
        // Act
        presenter.ChangePlayerPanelSettings(argument);
        // Assert
        settingService.DidNotReceiveWithAnyArgs().SaveSetting(argument);
        mock.DidNotReceiveWithAnyArgs().SetSettings(argument);
        view.DidNotReceiveWithAnyArgs().Close();
        messageParser.Received().GetMessage(MessageType.PlayerPanelSettingsInvalid);
        view.Received().ShowMessage(returnedMessage);
    }

    private void SetFormAsValid(bool isValid)
    {
        view.ValidateForm().Returns(isValid);
    }
}

I'm just testing all the methods in my presenter on calls. But as you can note I have useless string in my tests which related with testing settingService. Because I didn't link this settingsService to my presenter. So, we can write anything to this settingsService (Recieved, DidNotRecieved) it will be passed. And what I'm thinking about is modify my constructor in presenter and pass there somehow interface of concrete SettingsService and link it with concrete SettingsService. But it won't work, because our SettingsService demands a parameter in constructor (concrete SettingParser) and our dependecy resolver doesn't know about it.

So, I want make testable my SettingService in presenter. And actually it's my question - how to do it in the best way? I know that theme is pretty large and maybe it's hard to catch what I'm talking about from the first time, but I hope you can help me. Please ask me if I should explain something and thanks for your time!

Aucun commentaire:

Enregistrer un commentaire