Press "Enter" to skip to content

Lesson 11.1: Creating the Unit Test Project

As the game gets more features, it becomes more difficult to manually test. So, we will add a unit test project that can do fast, automated tests for us.

This will help us as we add more features. We can run the unit tests and see if the changes broke any of the pre-existing code.

Step 1: Create a new unit test project

Right-click on the SOSCSRPG solution (in Solution Explorer) and choose Add -> New Project -> Visual C# -> Test -> Unit Test Project (.NET Framework)

Name the unit test project “TestEngine”. There is no naming requirement for the solution, but I like to match my unit test project name with the name of the class library project it is testing.

Add reference in the TestEngine project to Engine project.

Delete UnitTest1.cs class – the default file created with project.

Create “ViewModels” folder in TestEngine class. This is not a .NET requirement. It’s just how I like to synchronize my tests projects with the class library project.

Step 2: Create new unit test class TestEngine\ViewModels\TestGameSession.cs

Right-click on the ViewModels folder in the TestEngine project and select Add -> New Item -> Visual C# Items -> Test -> Basic Unit Test. Name the file TestGameSession.cs

Add “using Engine.ViewModels;”, so we can access the GameSession class in the Engine.ViewModels namespace.

On line 6, notice that the unit test class has a “[TestClass]” attribute. This is how the unit test runner knows which classes have unit tests.

On lines 9 through 16 we have a unit test. Each unit tests needs to have the “[TestMethod]” attribute, to identify the function as a unit test. All unit test functions are “public void”.

On line 12 is the logic we are going to test. In this test, all we want to check is that instantiating a GameSession object will also instantiate a Player object, assign it to the CurrentPlayer property, and set the CurrentLocation to the town square.

The tests are the “Assert” statements on lines 14 and 15. These assertions are what we expect to happen, after we run the other code in the test function (in this case, only line 12)

On line 14, we assert that GameSession.CurrentPlayer should not be null. When the test runs, if this assertion is false (CurrentPlayer is null), then the test will fail.

On line 15, we assert that the CurrentLocation.Name should be “Town square”. If that assertion is not true, the test will fail.

If our assertions are correct – the values are what we expect – the test passes.

In the test on lines 18 through 27, we instantiate a GameSession object and apply 999 hit points of damage to the player. This should kill the player, causing them to move to their home location and completely heal (the things we test in the assertions on line 25 and 26).

This test exposed a problem. The GameSession.OnCurrentPlayerKilled function assumes the player was always killed by the CurrentMonster (line 293, where it displays the CurrentMonster.Name value).

We saw this type of problem before – when we had to put our RaiseMessage calls before the code that could change objects.

For now, let’s change line 293 of GameSession.cs to say, “You have been killed”. In the future, we may add other ways the player could be killed: poisons, spells, curses, etc. But, we will worry about making changes later, if we actually add those changes.

TestGameSession.cs
using Engine.ViewModels;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace TestEngine.ViewModels
{
    [TestClass]
    public class TestGameSession
    {
        [TestMethod]
        public void TestCreateGameSession()
        {
            GameSession gameSession = new GameSession();
            Assert.IsNotNull(gameSession.CurrentPlayer);
            Assert.AreEqual("Town square", gameSession.CurrentLocation.Name);
        }
        [TestMethod]
        public void TestPlayerMovesHomeAndIsCompletelyHealedOnKilled()
        {
            GameSession gameSession = new GameSession();
            gameSession.CurrentPlayer.TakeDamage(999);
            Assert.AreEqual("Home", gameSession.CurrentLocation.Name);
            Assert.AreEqual(gameSession.CurrentPlayer.Level * 10, gameSession.CurrentPlayer.CurrentHitPoints);
        }
    }
}
GameSession.cs
using System;
using System.Linq;
using Engine.EventArgs;
using Engine.Factories;
using Engine.Models;
namespace Engine.ViewModels
{
    public class GameSession : BaseNotificationClass
    {
        public event EventHandler<GameMessageEventArgs> OnMessageRaised;
        #region Properties
        private Player _currentPlayer;
        private Location _currentLocation;
        private Monster _currentMonster;
        private Trader _currentTrader;
        public World CurrentWorld { get; }
        public Player CurrentPlayer
        {
            get { return _currentPlayer; }
            set
            {
                if(_currentPlayer != null)
                {
                    _currentPlayer.OnLeveledUp -= OnCurrentPlayerLeveledUp;
                    _currentPlayer.OnKilled -= OnCurrentPlayerKilled;
                }
                _currentPlayer = value;
                if (_currentPlayer != null)
                {
                    _currentPlayer.OnLeveledUp += OnCurrentPlayerLeveledUp;
                    _currentPlayer.OnKilled += OnCurrentPlayerKilled;
                }
            }
        }
        public Location CurrentLocation
        {
            get { return _currentLocation; }
            set
            {
                _currentLocation = value;
                OnPropertyChanged();
                OnPropertyChanged(nameof(HasLocationToNorth));
                OnPropertyChanged(nameof(HasLocationToEast));
                OnPropertyChanged(nameof(HasLocationToWest));
                OnPropertyChanged(nameof(HasLocationToSouth));
                CompleteQuestsAtLocation();
                GivePlayerQuestsAtLocation();
                GetMonsterAtLocation();
                CurrentTrader = CurrentLocation.TraderHere;
            }
        }
        public Monster CurrentMonster
        {
            get { return _currentMonster; }
            set
            {
                if(_currentMonster != null)
                {
                    _currentMonster.OnKilled -= OnCurrentMonsterKilled;
                }
                
                _currentMonster = value;
                if(_currentMonster != null)
                {
                    _currentMonster.OnKilled += OnCurrentMonsterKilled;
                    RaiseMessage("");
                    RaiseMessage($"You see a {CurrentMonster.Name} here!");
                }
                OnPropertyChanged();
                OnPropertyChanged(nameof(HasMonster));
            }
        }
        public Trader CurrentTrader
        {
            get { return _currentTrader; }
            set
            {
                _currentTrader = value; 
                
                OnPropertyChanged();
                OnPropertyChanged(nameof(HasTrader));
            }
        }
        public Weapon CurrentWeapon { get; set; }
        public bool HasLocationToNorth => 
            CurrentWorld.LocationAt(CurrentLocation.XCoordinate, CurrentLocation.YCoordinate + 1) != null;
        public bool HasLocationToEast => 
            CurrentWorld.LocationAt(CurrentLocation.XCoordinate + 1, CurrentLocation.YCoordinate) != null;
        public bool HasLocationToSouth => 
            CurrentWorld.LocationAt(CurrentLocation.XCoordinate, CurrentLocation.YCoordinate - 1) != null;
        public bool HasLocationToWest => 
            CurrentWorld.LocationAt(CurrentLocation.XCoordinate - 1, CurrentLocation.YCoordinate) != null;
        public bool HasMonster => CurrentMonster != null;
        public bool HasTrader => CurrentTrader != null;
        #endregion
        public GameSession()
        {
            CurrentPlayer = new Player("Scott", "Fighter", 0, 10, 10, 1000000);
            if (!CurrentPlayer.Weapons.Any())
            {
                CurrentPlayer.AddItemToInventory(ItemFactory.CreateGameItem(1001));
            }
            CurrentWorld = WorldFactory.CreateWorld();
            CurrentLocation = CurrentWorld.LocationAt(0, 0);
        }
        public void MoveNorth()
        {
            if(HasLocationToNorth)
            {
                CurrentLocation = CurrentWorld.LocationAt(CurrentLocation.XCoordinate, CurrentLocation.YCoordinate + 1);
            }
        }
        public void MoveEast()
        {
            if(HasLocationToEast)
            {
                CurrentLocation = CurrentWorld.LocationAt(CurrentLocation.XCoordinate + 1, CurrentLocation.YCoordinate);
            }
        }
        public void MoveSouth()
        {
            if(HasLocationToSouth)
            {
                CurrentLocation = CurrentWorld.LocationAt(CurrentLocation.XCoordinate, CurrentLocation.YCoordinate - 1);
            }
        }
        public void MoveWest()
        {
            if(HasLocationToWest)
            {
                CurrentLocation = CurrentWorld.LocationAt(CurrentLocation.XCoordinate - 1, CurrentLocation.YCoordinate);
            }
        }
        private void CompleteQuestsAtLocation()
        {
            foreach(Quest quest in CurrentLocation.QuestsAvailableHere)
            {
                QuestStatus questToComplete =
                    CurrentPlayer.Quests.FirstOrDefault(q => q.PlayerQuest.ID == quest.ID &&
                                                             !q.IsCompleted);
                if(questToComplete != null)
                {
                    if(CurrentPlayer.HasAllTheseItems(quest.ItemsToComplete))
                    {
                        // Remove the quest completion items from the player's inventory
                        foreach (ItemQuantity itemQuantity in quest.ItemsToComplete)
                        {
                            for(int i = 0; i < itemQuantity.Quantity; i++)
                            {
                                CurrentPlayer.RemoveItemFromInventory(CurrentPlayer.Inventory.First(item => item.ItemTypeID == itemQuantity.ItemID));
                            }
                        }
                        RaiseMessage("");
                        RaiseMessage($"You completed the '{quest.Name}' quest");
                        // Give the player the quest rewards
                        RaiseMessage($"You receive {quest.RewardExperiencePoints} experience points");
                        CurrentPlayer.AddExperience(quest.RewardExperiencePoints);
                        RaiseMessage($"You receive {quest.RewardGold} gold");
                        CurrentPlayer.ReceiveGold(quest.RewardGold);
                        foreach(ItemQuantity itemQuantity in quest.RewardItems)
                        {
                            GameItem rewardItem = ItemFactory.CreateGameItem(itemQuantity.ItemID);
                            RaiseMessage($"You receive a {rewardItem.Name}");
                            CurrentPlayer.AddItemToInventory(rewardItem);
                        }
                        // Mark the Quest as completed
                        questToComplete.IsCompleted = true;
                    }
                }
            }
        }
        private void GivePlayerQuestsAtLocation()
        {
            foreach(Quest quest in CurrentLocation.QuestsAvailableHere)
            {
                if(!CurrentPlayer.Quests.Any(q => q.PlayerQuest.ID == quest.ID))
                {
                    CurrentPlayer.Quests.Add(new QuestStatus(quest));
                    RaiseMessage("");
                    RaiseMessage($"You receive the '{quest.Name}' quest");
                    RaiseMessage(quest.Description);
                    RaiseMessage("Return with:");
                    foreach(ItemQuantity itemQuantity in quest.ItemsToComplete)
                    {
                        RaiseMessage($"   {itemQuantity.Quantity} {ItemFactory.CreateGameItem(itemQuantity.ItemID).Name}");
                    }
                    RaiseMessage("And you will receive:");
                    RaiseMessage($"   {quest.RewardExperiencePoints} experience points");
                    RaiseMessage($"   {quest.RewardGold} gold");
                    foreach(ItemQuantity itemQuantity in quest.RewardItems)
                    {
                        RaiseMessage($"   {itemQuantity.Quantity} {ItemFactory.CreateGameItem(itemQuantity.ItemID).Name}");
                    }
                }
            }
        }
        private void GetMonsterAtLocation()
        {
            CurrentMonster = CurrentLocation.GetMonster();
        }
        public void AttackCurrentMonster()
        {
            if (CurrentWeapon == null)
            {
                RaiseMessage("You must select a weapon, to attack.");
                return;
            }
            // Determine damage to monster
            int damageToMonster = RandomNumberGenerator.NumberBetween(CurrentWeapon.MinimumDamage, CurrentWeapon.MaximumDamage);
            if (damageToMonster == 0)
            {
                RaiseMessage($"You missed the {CurrentMonster.Name}.");
            }
            else
            {
                RaiseMessage($"You hit the {CurrentMonster.Name} for {damageToMonster} points.");
                CurrentMonster.TakeDamage(damageToMonster);
            }
            if(CurrentMonster.IsDead)
            {
                // Get another monster to fight
                GetMonsterAtLocation();
            }
            else
            {
                // Let the monster attack
                int damageToPlayer = RandomNumberGenerator.NumberBetween(CurrentMonster.MinimumDamage, CurrentMonster.MaximumDamage);
                if (damageToPlayer == 0)
                {
                    RaiseMessage($"The {CurrentMonster.Name} attacks, but misses you.");
                }
                else
                {
                    RaiseMessage($"The {CurrentMonster.Name} hit you for {damageToPlayer} points.");
                    CurrentPlayer.TakeDamage(damageToPlayer);
                }
            }
        }
        private void OnCurrentPlayerKilled(object sender, System.EventArgs eventArgs)
        {
            RaiseMessage("");
            RaiseMessage("You have been killed.");
            CurrentLocation = CurrentWorld.LocationAt(0, -1);
            CurrentPlayer.CompletelyHeal();
        }
        private void OnCurrentMonsterKilled(object sender, System.EventArgs eventArgs)
        {
            RaiseMessage("");
            RaiseMessage($"You defeated the {CurrentMonster.Name}!");
            RaiseMessage($"You receive {CurrentMonster.RewardExperiencePoints} experience points.");
            CurrentPlayer.AddExperience(CurrentMonster.RewardExperiencePoints);
            RaiseMessage($"You receive {CurrentMonster.Gold} gold.");
            CurrentPlayer.ReceiveGold(CurrentMonster.Gold);
            foreach(GameItem gameItem in CurrentMonster.Inventory)
            {
                RaiseMessage($"You receive one {gameItem.Name}.");
                CurrentPlayer.AddItemToInventory(gameItem);
            }
        }
        private void OnCurrentPlayerLeveledUp(object sender, System.EventArgs eventArgs)
        {
            RaiseMessage($"You are now level {CurrentPlayer.Level}!");
        }
        private void RaiseMessage(string message)
        {
            OnMessageRaised?.Invoke(this, new GameMessageEventArgs(message));
        }
    }
}

NEXT LESSON: Lesson 12.1: Making the GameItem class more flexible

PREVIOUS LESSON: Lesson 10.6: Clean up property setters and PropertyChanged notifications

6 Comments

  1. Wicked Cat
    Wicked Cat 2023-05-08

    Hello, sorry, I’m getting an error in the TestEngine project. Google is not helping I’m just getting confused lol

    Project ‘..\Engine\Engine.csproj’ targets ‘net6.0’. It cannot be referenced by a project that targets ‘.NETFramework,Version=v4.7.2’. TestEngine

    • SOSCSRPG
      SOSCSRPG 2023-05-08

      When you created the Engine project, you created it as a “.NET 6.0” Class Library. But, it looks like when you’re trying to create the unit test project, you selected the “.NET Framework” Test project type. Delete the current Test project from the solution and re-add it as a unit test project that says “.NET 6” or “.NET Core”.

      .NET Core and .NET 6.0/7.0 are the newer versions of .NET, and the older .NET Framework projects can’t reference them.

      Please let me know if that wasn’t clear, or if there are any problems with that.

    • Chris
      Chris 2023-09-07

      I’ve been having the same problem as you, after much fussing & fiddling around, here’s what seemed to solve it for me.
      When adding the test project, don’t select “Unit Test Project (.NET Framework)”. When I do this, it only gives the options of target frameworks 4.8 & 4.7.2. Instead choose MSTest Test Project. This allowed me to select NET 6.0.
      However even this still didn’t work for me at first for some reason. When I added the using directive, it told me it couldn’t see Engine.ViewModels, even though I’d added the reference. After I unloaded and reloaded it (right-click TestProject > Unload Project – the option is towards the bottom – then right-click and Reload), it /finally/ worked. I’ve been struggling with this since last night. Hope this helps.

  2. Steven
    Steven 2024-01-07

    SOSCSRPG is GameSession in a way the same as your other tutorial? Speaking of the super adventure.cs or something like that.later on in that code isn’t is basicly GameSession from your newer project after a certain point? Because am confused after a certain point super adventure.something seems like it becames GameSession after a certain point or something alike to it?.

    • SOSCSRPG
      SOSCSRPG 2024-01-07

      Yes, this project is basically the same as the SuperAdventure project, just using WPF and a newer version of .NET. Some people asked to see how to create the game in WPF, so that’s why I re-did it here.

  3. Steven
    Steven 2024-01-07

    I mean after a certain point the code in both starts looking alike and doing a lot of the something as in the is the same with the other one and vice versa.

Leave a Reply

Your email address will not be published. Required fields are marked *