Dependency Injection — A Practical Guide to Improving the Flexibility and Testability of Code

Durjoy Acharjya
7 min readMar 14, 2024

--

Dependency Injection is very intimidating for beginners. Well, it has a fancy name, and when people try to teach it, they usually do so later in the learning process. I’ve also noticed that they frequently overcomplicate internet examples. Today, I would like to take some basic code written in a style that would be understandable to a beginner and easily rewrite it to make use of dependency injection.

Dependency Injection in C# | Durjoy Acharya

Let’s now go to fundamental ideas using explicit code examples.

//Simple color picker game
namespace DependencyInjection
{
public enum Options
{
RED, GREEN, BLUE
}
public enum Result
{
Human, Computer, Draw
}
public class GameChanger
{
private Random _rand = new Random();
private Options _human;
//Player Human
public Result PlayGame()
{
string? input = "";
do
{
Console.Write($"{Environment.NewLine} Enter Option: (R)ed, (G)reen or (B)lue !");
input = Console.ReadLine()?.ToUpper();

switch (input)
{
case "R":
_human = Options.RED;
break;
case "G":
_human = Options.GREEN;
break;
case "B":
_human = Options.BLUE;
break;
default:
Console.WriteLine($"{Environment.NewLine} Wrong Option. Please try again!");
break;
}
} while (input != "R" && input != "G" && input != "B");

//Player 2 Computer
Options _computer = (Options)_rand.Next(0, 3);
Console.Write($"{Environment.NewLine}Player 2 Picked {_computer.ToString()}");
if (_human.Equals(_computer))
return Result.Draw;

//Some Dummy logic
if (_human == Options.RED && _computer == Options.GREEN
|| _human == Options.BLUE && _computer == Options.RED || _human == Options.GREEN && _computer == Options.BLUE)
return Result.Human;
return Result.Computer;
}
}
}
var gc = new GameChanger();
do
{
var result = gc.PlayGame();

switch (result)
{
case Result.Human:
Console.WriteLine($"{Environment.NewLine} Player 1 Wins");
break;
case Result.Computer:
Console.WriteLine($"{Environment.NewLine} Player 2 Wins");
break;
default:
Console.WriteLine($"{Environment.NewLine} It's a Draw!");
break;
}
} while (Console.ReadLine()?.ToUpper() == "Y");

Here is an example of two code files a GameChanger.cs & a Driver Code Program.cs file

Now here the GameChanger is just a very minimalistic class that plays a round of RED, GREEN, BLUE(RGB) and here we have two enums

Okay, Everything is pretty good right now & run successfully. !Ok wait there are serious problems first one is how do we test the answer we can’t automate code test on the PlayGame() method first control goes to

input = Console.ReadLine()?.ToUpper();

This line line required human input to run and that can easily be automated

Secondly, the computer gets its data from the random number from

private Random _rand = new Random();

Here we can’t control what is computer probably picked if we can’t control what the computer picks that means we can’t write test logic for it.

Okay, then how can we test this code there is only one way which is the brute force approach, and running over N over again can be done millions of times.

That is not a good approach that is different from how professionals like to write code.

Okay, As a beginner you might be thinking that, oh I can apply some params, booleans, and if-else statements to the PlayGame() method which is inside the GameChanger Class. That is so bad approach because now any time in the future you want to change the behavior of how the Options are received or any features. You can’t think how big and complicated the class is as you add more and more if-else statements you need all permutational combinations (RED, GREEN, BLUE).

Here is a case of a situation the Dependency Injection concept comes in.

Does the GameChanger need to care about how players 1 and 2 arrive at their options? is the question we need to ask.

The answer is really not gonna happen in real life. It doesn’t need to care what the GameChanger Doing here Player 1 gives me your option, Hey player 2 gives me your option It doesn’t care about where it comes from console or random or whatever.

So How to overcome this situation?

let’s talk about the term dependency injection this GameChanger PlayGame() method it depends on two choices being made you cannot play around with RED, GREEN, or BLUE without getting those two choices but what we don’t want it to do is be concerned with these details it should not be managing the process of how player one determines its choice and it shouldn’t be concerned with how player 2 gets its Choice it should just ask for them so what we’re going to do is we’re going to pull this code out of the GameChanger class it’s still going to exist but we’re going to put it in different objects and those become like swappable components so dependency injection it depends on a choice and because we’ve moved the code somewhere else we’re going to inject the code at runtime into the GameChanger we’re going to say, Hey GameChager here are the two objects that are going to give you. Your options that you need and the GameChanger is going to say great object one give me your choice and that code will execute and it will pass the choice up and then it will say hey object two give me your choice and that object’s code will execute and it will give it the choice the GameChanger will not see or care where that choice came from anymore it depends on it but the objects are going to be injected so when you think about dependency injection as a term all we’re talking about is being able to swap in objects that have different implementations which is how they do their jobs now as we jump over to code the first step of dependency injection is taking that behavior we want to swap and expressing it as a method because that’s what an interface really is it is a list of methods and behaviors that every object that implements that interface must have now for RED,GREEN,BLUE here this is really simple it’s only one method it needs to get a choice from a player

Now move on to the real thrill….

namespace DependencyInjectionTests
{
public enum Options
{
RED,
GREEN,
BLUE
}

public enum Result
{
Human,
Computer,
Draw
}

public interface IGameLogic
{
Result PlayGame(Options humanChoice, Options computerChoice);
}

public class RGBGame : IGameLogic
{
public Result PlayGame(Options humanChoice, Options computerChoice)
{
if (humanChoice.Equals(computerChoice))
{
return Result.Draw;
}

// Implement game logic (assuming RED > GREEN, GREEN > BLUE, BLUE > RED)
//Dummy logic you need not to follow it
return humanChoice switch
{
Options.RED when computerChoice == Options.GREEN => Result.Computer,
Options.GREEN when computerChoice == Options.BLUE => Result.Computer,
Options.BLUE when computerChoice == Options.RED => Result.Computer,
_ => Result.Human
};
}
}
}
using System;

namespace DependencyInjectionTests
{
public class UserInputHandler
{
public Options GetPlayerChoice()
{
string? input;
do
{
Console.Write($"{Environment.NewLine} Enter Option: (R)ed, (G)reen or (B)lue !");
input = Console.ReadLine().ToUpper();
switch (input)
{
case "R":
return Options.RED;
case "G":
return Options.GREEN;
case "B":
return Options.BLUE;
default:
Console.WriteLine($"{Environment.NewLine} Wrong Option. Please try again!");
break;
}
} while (true);
}
}
}
namespace DependencyInjection
{
public class UserInputHandler
{
public Options GetPlayerChoice()
{
string? input;
do
{
Console.Write($"{Environment.NewLine} Enter Option: (R)ed, (G)reen or (B)lue !");
input = Console.ReadLine()?.ToUpper();
switch (input)
{
case "R":
return Options.RED;
case "G":
return Options.GREEN;
case "B":
return Options.BLUE;
default:
Console.WriteLine($"{Environment.NewLine} Wrong Option. Please try again!");
break;
}
} while (true);
}
}

}
using System;

namespace DependencyInjectionTests
{
public class GameEngine
{

private readonly IGameLogic _gameLogic;
private readonly UserInputHandler _userInputHandler;
//constructor injection
public GameEngine(IGameLogic gameLogic, UserInputHandler userInputHandler)
{
_gameLogic = gameLogic;
_userInputHandler = userInputHandler;
}
public void PlayGame()
{
do
{
var humanChoice = _userInputHandler.GetPlayerChoice();
var computerChoice = (Options)new Random().Next(0, 3);
Console.Write($"{Environment.NewLine}Player 2 Picked: {computerChoice.ToString()}");

var result = _gameLogic.PlayGame(humanChoice, computerChoice);

switch (result)
{
case Result.Human:
Console.Write($"{Environment.NewLine} Player 1 Wins!");
break;
case Result.Computer:
Console.Write($"{Environment.NewLine} Player 2 Wins!");
break;
default:
Console.Write($"{Environment.NewLine} It's a Draw!");
break;
}
Console.Write($"{Environment.NewLine} Play Again! Press (Y)!");

} while (Console.ReadLine()?.ToUpper() == "Y");
}
}
}

//Program.cs Top Level View
var gameLogic = new RGBGame();
var userInputHandler = new UserInputHandler();
var game = new GameEngine(gameLogic, userInputHandler);
game.PlayGame();

Look Very Carefully, here Introduce an interface IGameLogic to encapsulate the core game logic and dependency injection. Create a RGBGame class that implements IGameLogic and provides the actual game logic. Design a UserInputHandler class to handle user input and return the player's choice as an Options enum. Inject the IGameLogic and UserInputHandler dependencies into the Game class through its constructor. Now we can easily update the future implementation, flexibility, and maintainability easily test the code

Now we move on to unit tests of this code from different aspect

using Xunit;
namespace DependencyInjectionTests
{
public class UserInputHandlerTests
{
[Fact]
public void ReturnsCorrectOptionForValidInput()
{
var userInputHandler = new UserInputHandler();
Assert.Equal(Options.RED, userInputHandler.GetPlayerChoice()); // Simulate user input "R"
Assert.Equal(Options.GREEN, userInputHandler.GetPlayerChoice()); // Simulate user input "G"
Assert.Equal(Options.BLUE, userInputHandler.GetPlayerChoice()); // Simulate user input "B"
}

}
}
using System;
using Xunit;

namespace DependencyInjectionTests
{
public class RGBGameTests
{
[Fact]
public void DrawWhenBothPlayersPickSameOption()
{
var gameLogic = new RGBGame();
Assert.Equal(Result.Draw, gameLogic.PlayGame(Options.RED, Options.RED));
Assert.Equal(Result.Draw, gameLogic.PlayGame(Options.GREEN, Options.GREEN));
Assert.Equal(Result.Draw, gameLogic.PlayGame(Options.BLUE, Options.BLUE));
}

[Fact]
public void HumanWinsWhenValidCombinations()
{
var gameLogic = new RGBGame();
Assert.Equal(Result.Human, gameLogic.PlayGame(Options.RED, Options.GREEN));
Assert.Equal(Result.Human, gameLogic.PlayGame(Options.BLUE, Options.RED));
Assert.Equal(Result.Human, gameLogic.PlayGame(Options.GREEN, Options.BLUE));
}
[Fact]
public void ComputerWinsWhenValidCombinations()
{
var gameLogic = new RGBGame();
Assert.Equal(Result.Computer, gameLogic.PlayGame(Options.GREEN, Options.RED));
Assert.Equal(Result.Computer, gameLogic.PlayGame(Options.RED, Options.BLUE));
Assert.Equal(Result.Computer, gameLogic.PlayGame(Options.BLUE, Options.GREEN));
}
[Fact]
public void InvalidInputHandling()
{
// Simulate unexpected input for game logic testing
var gameLogic = new RGBGame();

// Assert that an exception is thrown or appropriate error handling occurs
Assert.Throws<ArgumentException>(() => gameLogic.PlayGame(Options.RED, (Options)10)); // Invalid computer choice
}
}
}
using Moq;
using Xunit;

namespace DependencyInjectionTests
{
public class GameTests
{
[Fact]
public void PlaysGameWithMockedDependencies()
{
// Mock dependencies
var mockGameLogic = new Mock<IGameLogic>();
var mockUserInputHandler = new Mock<UserInputHandler>();

// Set up expected behavior of mocks
mockGameLogic.Setup(logic => logic.PlayGame(It.IsAny<Options>(), It.IsAny<Options>()))
.Returns(Result.Human);
mockUserInputHandler.Setup(handler => handler.GetPlayerChoice())
.Returns(Options.RED);

// Create the game with mocked dependencies
var game = new GameEngine(mockGameLogic.Object, mockUserInputHandler.Object);

// Play the game and verify interactions
game.PlayGame();

// Assertions (e.g., verify mock method calls, output)
mockGameLogic.Verify(logic => logic.PlayGame(Options.RED, It.IsAny<Options>()), Times.Once);
mockUserInputHandler.Verify(handler => handler.GetPlayerChoice(), Times.Once);
}
}
}

Here I use xUnit you can choose your favorite test tool

In this blog, I'm trying to explain Dependency Injection and Improving the Flexibility and Testability of Code please feedback if there is any issue. Thank you guys

Source Code: Dependency Injection

Follow me on Github Linkedin

--

--

Durjoy Acharjya
Durjoy Acharjya

Written by Durjoy Acharjya

🚀 Passionate Programmer | 🌐 Angular | ⚡ .NET Core | ☁️ Spring Boot | 🔧 AWS | 🔒 DevOps | 🌱 MLOps | 🛡️ DevSecOps | 📊 T-SQL | 📡 Kafka 📱 Swift | 🍏 iOS

No responses yet