Benefits of SOLID Principles and Their Application in Testing

The main benefits of complying with SOLID principles are reduced software product costs and increased competitiveness of the software product and team.



How is cost reduction and increased competitiveness achieved?





Let's quickly go over the SOLID principles from a business perspective:



  1. The Single Responsibility Principle.

    If the code complies with this principle, then it is easier to understand, debug, modify, and test it. You can make more ambitious changes, because if something breaks, then something will be only one function, and not the whole system. In addition, one function is easier to cover with tests, and check that after the changes, nothing has broken.
  2. The principle of openness / closedness (The Open Closed Principle).

    If the code complies with this principle, the addition of new functionality will require minimal changes to the existing code. This means that it reduces the time for refactoring.
  3. The substitution principle of Barbara Liskov (The Liskov Substitution Principle).

    Subject to this principle, it is possible to inherit classes by classes, to replace classes of parents, without rewriting another code. Compliance with this principle facilitates compliance with the principle of openness of closure. As a result, saving time and money.
  4. The Interface Segregation Principle

    This principle echoes the principle of sole responsibility. By breaking up the interfaces as intended, we do not force them to implement unnecessary functions in classes that implement interfaces, so there will be less code, which will affect the speed of implementation, and save money.
  5. The Dependency Inversion Principle

    Compliance with the principle provides flexibility of the program, and allows you to replace some classes with other classes, provided that the classes implement a common interface. This allows you to write unit tests. Compliance with this principle is highly desirable for writing testable code. And the test code is needed to reduce reputation risks, facilitate refactoring, and save time on debugging.


It is assumed that you are familiar with the principles of SOLID, so they will not be described. In addition, there are many articles explaining the principles of SOLID.



Further, we will focus on the application of the principles of SOLID, to reduce losses from errors in the software product, due to more convenient testing.



Consider a code that guesses a number conceived by a person:



private void LegacyCode_Click(object sender, EventArgs e) { _minNum = 1; _maxNum = 100; do { int medNum = (_minNum + _maxNum) / 2; var dr = MessageBox.Show($"   {medNum} ?", "", MessageBoxButtons.YesNo); if (dr == DialogResult.Yes) _minNum = medNum + 1; else _maxNum = medNum; if (_maxNum == _minNum) { MessageBox.Show($"  {_minNum}!"); } } while (_maxNum != _minNum); }
      
      





Why can't I write a unit test on the LegacyCode_Click function?



This code is heavily dependent on the static MessageBox class, which interacts with an external protein system (human), which is not available when running unit tests on the build server. In other words, the function depends on the person who cannot be included in the unit test runtime.



How can we eliminate the dependence of code execution on a person, or any external system that is undesirable to integrate into the unit testing runtime?



Answer: observe the principle of DIP (the Dependency Inversion Principle) principle of dependency inversion.



 public void DoPlayGame() { do { int medNum = (MinNum + MaxNum) / 2; var dr = _mesageBox.Show($"   {medNum} ?", "", MessageBoxButtons.YesNo); if (dr == DialogResult.Yes) MinNum = medNum + 1; else MaxNum = medNum; if (MaxNum == MinNum) { _mesageBox.Show($"  {MinNum} !"); } } while (MaxNum != MinNum); };
      
      





What has changed in the method? MessageBox has been replaced by _mesageBox. But if "MessageBox" is a static class that interacts with the user, then "_mesageBox" is a class property declared through the interface in the class:



 public class GameDiMonolit { private IMessageBoxAdapter _mesageBox; public int MinNum { get; set; } public int MaxNum { get; set; } public GameDiMonolit(IMessageBoxAdapter mesageBox) { _mesageBox = mesageBox; } public void DoPlayGame() { do { int medNum = (MinNum + MaxNum) / 2; var dr = _mesageBox.Show($"   {medNum} ?", "", MessageBoxButtons.YesNo); if (dr == DialogResult.Yes) MinNum = medNum + 1; else MaxNum = medNum; if (MaxNum == MinNum) { _mesageBox.Show($"  {MinNum} !"); } } while (MaxNum != MinNum); } } }
      
      





Declaring a variable "_mesageBox" with an interface type is the inverse of the dependency. When _mesageBox is declared from a specific class, the compiler rigidly binds the code to a specific class of the variable, and declaring a variable from the interface gives freedom to use any class instances that implement the interface.



To interact with the user, you need to implement a class that implements "IMessageBoxAdapter":



 public interface IMessageBoxAdapter { DialogResult Show(string mes); DialogResult Show(string text, string caption, MessageBoxButtons buttons); }
      
      





We implement it through the adapter pattern:



 public class MessageBoxAdapter : IMessageBoxAdapter { public DialogResult Show(string mes) { return MessageBox.Show(mes); } public DialogResult Show(string text, string caption, MessageBoxButtons buttons) { return MessageBox.Show(text, caption, buttons); } }
      
      





A call from the form will be, for example, like this:



 private void btnDiInvCode_Click(object sender, EventArgs e) { var _gameDiMonolit = new GameDiMonolit(new MessageBoxAdapter()); _gameDiMonolit.MinNum = 1; _gameDiMonolit.MaxNum = 100; _gameDiMonolit.DoPlayGame(); }
      
      





You can implement various “MessageBoxAdapter” that can access different classes, and the “DoPlayGame” method will not know anything about it.



Let’s now look at how you can test the DoPlayGame method. What are the difficulties? The method contains a loop, and the loop may loop. Fortunately, NUnit has a “Timeout” parameter. Since the “DoPlayGame” inside the loop contains branches, it is an if statement, and a condition in while, then you need to somehow emulate user button clicks in the test. At the same time, clicking on the buttons should be thought out to ensure that all branches of the code are covered.



For testing, you can implement the specialized class “MessageBoxList”, which substitutes user responses from the queue:



 public class MessageBoxList : IMessageBoxAdapter { private Queue<DialogResult> _queueDialogResult; private List<string> _listCaption; private List<string> _listText; public List<string> ListCaption => _listCaption; public List<string> ListText => _listText; public Queue<DialogResult> QueueDialogResult => _queueDialogResult; public MessageBoxList() { _listText = new List<string>(); _listCaption = new List<string>(); _queueDialogResult = new Queue<DialogResult>(); } public DialogResult Show(string text) { _listText.Add(text); return DialogResult.OK; } public DialogResult Show(string text, string caption, MessageBoxButtons buttons) { _listText.Add(text); _listCaption.Add(caption); return _queueDialogResult.Dequeue(); } }
      
      





Then the test will be like this:



 [Test(),Timeout(5000)/*   ,      */] public void DoPlayGameWithMessageBoxListTest() { // var messageBoxList = new MessageBoxList(); var gameDiMonolit = new GameDiMonolit(messageBoxList); gameDiMonolit.MinNum = 10; gameDiMonolit.MaxNum = 40; messageBoxList.QueueDialogResult.Enqueue(DialogResult.Yes); messageBoxList.QueueDialogResult.Enqueue(DialogResult.Yes); messageBoxList.QueueDialogResult.Enqueue(DialogResult.No); messageBoxList.QueueDialogResult.Enqueue(DialogResult.No); messageBoxList.QueueDialogResult.Enqueue(DialogResult.Yes); //  gameDiMonolit.DoPlayGame(); var etalonList = new List<string>() { "   25 ?", "   33 ?", "   37 ?", "   35 ?", "   34 ?", "  35 !" }; Assert.True(etalonList.SequenceEqual(messageBoxList.ListText), "."); }
      
      





However, you can not write a specialized class for testing, but use the Moq library, in this case, the test will become like this:



 [Test(), Timeout(5000)/*   ,      */] public void DoPlayGameWithMoqTest() { // var moqMessageBoxList = new Moq.Mock<IMessageBoxAdapter>(); var gameDiMonolit = new GameDiMonolit(moqMessageBoxList.Object); moqMessageBoxList.Setup(a => a.Show("   25 ?", "", MessageBoxButtons.YesNo)).Returns(DialogResult.Yes); moqMessageBoxList.Setup(a => a.Show("   33 ?", "", MessageBoxButtons.YesNo)).Returns(DialogResult.Yes); moqMessageBoxList.Setup(a => a.Show("   37 ?", "", MessageBoxButtons.YesNo)).Returns(DialogResult.No); moqMessageBoxList.Setup(a => a.Show("   35 ?", "", MessageBoxButtons.YesNo)).Returns(DialogResult.No); moqMessageBoxList.Setup(a => a.Show("   34 ?", "", MessageBoxButtons.YesNo)).Returns(DialogResult.Yes); gameDiMonolit.MinNum = 10; gameDiMonolit.MaxNum = 40; //  gameDiMonolit.DoPlayGame(); moqMessageBoxList.Verify(a => a.Show("   25 ?", "", MessageBoxButtons.YesNo), Moq.Times.Once); moqMessageBoxList.Verify(a => a.Show("   33 ?", "", MessageBoxButtons.YesNo), Moq.Times.Once); moqMessageBoxList.Verify(a => a.Show("   37 ?", "", MessageBoxButtons.YesNo), Moq.Times.Once); moqMessageBoxList.Verify(a => a.Show("   35 ?", "", MessageBoxButtons.YesNo), Moq.Times.Once); moqMessageBoxList.Verify(a => a.Show("   34 ?", "", MessageBoxButtons.YesNo), Moq.Times.Once); moqMessageBoxList.Verify(a => a.Show("  35 !"), Moq.Times.Once); }
      
      





What is the fundamental difference between a test with the specialized MessageBoxList class and a test using Moq?



Answer: Method with spec. The MessageBoxList class provides more flexibility, and it allows you to control the sequence of responses of the test method to the user. A test using Moq simply checks for answers, but in what order they came, it does not check.



As you can see, to write the tests I had to think a little, and the tests should be simple, and written almost mechanically. This is quite achievable if, while writing the code, one more SOLID principle is observed, which was violated, namely the sole responsibility. What responsibilities can be distinguished in this method?



 public void DoPlayGame() { do { //: ( ) int medNum = (MinNum + MaxNum) / 2; var dr = _mesageBox.Show($"   {medNum} ?", "", MessageBoxButtons.YesNo); if (dr == DialogResult.Yes) MinNum = medNum + 1; else MaxNum = medNum; //:  ( ) if (MaxNum == MinNum) { _mesageBox.Show($"  {MinNum} !"); } } while (MaxNum != MinNum); //:   } } }
      
      





We highlight responsibility in other classes:



 //:   public class GameCycle { public IGameQuestion GameQuestion; private GameCycle() { } public GameCycle(IGameQuestion gameLogic) { GameQuestion = gameLogic; } public void Cycle() { while (!GameQuestion.RegularQuestion()); } } //:  public interface IGameQuestion { int MaxNum { get; set; } int MinNum { get; set; } bool RegularQuestion(); bool FinalQuestion(); } //:  public class GameQuestion : IGameQuestion { IMessageBoxAdapter _mesageBox; private GameQuestion() { } public int MinNum { get; set; } public int MaxNum { get; set; } public GameQuestion(IMessageBoxAdapter mesageBox) { _mesageBox = mesageBox; } //  public bool RegularQuestion() { int medNum = (MinNum + MaxNum) / 2; var dr = _mesageBox.Show($"   {medNum} ?", "", MessageBoxButtons.YesNo); if (dr == DialogResult.Yes) MinNum = medNum + 1; else MaxNum = medNum; bool res = FinalQuestion(); return res; } //  public bool FinalQuestion() { bool res = false; if (MaxNum == MinNum) { res = true; _mesageBox.Show($"  {MinNum} !"); } return res; } }
      
      





Call from program code:



 private void btnSOLIDcode_Click(object sender, EventArgs e) { var _gameSolid = new GameCycle(new GameQuestion(new MessageBoxAdapter())); _gameSolid.GameQuestion.MinNum = 1; _gameSolid.GameQuestion.MaxNum = 100; _gameSolid.Cycle(); }
      
      





We have one method, divided into several simple classes, let's see what tests we get:



 [TestFixture()] public class GameQuestionTests { [Test()] ///  ,    Yes public void RegularQuestionIntervalNeighboringNumbersYesTest() { // var moqMessageBoxList = new Moq.Mock<IMessageBoxAdapter>(); moqMessageBoxList.Setup(a => a.Show("   23 ?", "", MessageBoxButtons.YesNo)).Returns(DialogResult.Yes); var mes = new GameQuestion(moqMessageBoxList.Object); mes.MinNum = 23; mes.MaxNum = 24; //  mes.RegularQuestion(); Assert.AreEqual(24, mes.MinNum); Assert.AreEqual(24, mes.MaxNum); } [Test()] ///  ,    Yes public void RegularQuestionIntervalNeighboringNumbersNoTest() { // var moqMessageBoxList = new Moq.Mock<IMessageBoxAdapter>(); moqMessageBoxList.Setup(a => a.Show("   23 ?", "", MessageBoxButtons.YesNo)).Returns(DialogResult.No); var mes = new GameQuestion(moqMessageBoxList.Object); mes.MinNum = 23; mes.MaxNum = 24; //  mes.RegularQuestion(); Assert.AreEqual(23, mes.MinNum); Assert.AreEqual(23, mes.MaxNum); } [Test()] /// ,   public void FinalQuestionMinEqMaxTest() { var moqMessageBoxList = new Moq.Mock<IMessageBoxAdapter>(); var GameLogic = new GameQuestion(moqMessageBoxList.Object); GameLogic.MinNum = 23; GameLogic.MaxNum = 23; //  GameLogic.FinalQuestion(); moqMessageBoxList.Verify(a => a.Show("  23 !"), Moq.Times.Once); } [Test()] /// ,    public void FinalQuestionMinNoEqMaxTest() { var moqMessageBoxList = new Moq.Mock<IMessageBoxAdapter>(); var GameLogic = new GameQuestion(moqMessageBoxList.Object); GameLogic.MinNum = 23; GameLogic.MaxNum = 24; //  GameLogic.FinalQuestion(); moqMessageBoxList.Verify(a => a.Show(Moq.It.IsAny<string>()), Moq.Times.Never); } } [Test(), Timeout(5000)/*   ,      */] public void CycleTest() { // var gameLogic = new Moq.Mock<IGameQuestion>(); gameLogic.Setup(a => a.RegularQuestion()).Returns(true); var gameCycle = new GameCycle(gameLogic.Object); //  gameCycle.Cycle(); gameLogic.Verify(a => a.RegularQuestion(), Moq.Times.Once()); }
      
      





What is the difference between a test method with unallocated responsibilities and tests where each responsibility has its own class?



Answer: In that each test in the second case checks one responsibility, and this can be done almost mechanically. You can write more focused tests. In the first case, when two responsibilities are concentrated in one method, attention is focused on all two responsibilities, as a result of which the tests are more difficult to write, and most importantly, they turn out to be of lower quality.



Thank you for your attention and feedback in the comments.

It is very important for me to know what can be improved.



All Articles