Now this is often overlooked by most people, as testing is something seen as being sooooo far down the road that people wont think about it until the last moment. Now I will pretty much be speaking to the devs from this point on as we discuss AUTOMATED TESTS!!?! (BY GOSH!) and what they can do for you!
So lets take a scenario, you are making a space game, and you have lots of different guns with lots of different effects, some plasma some physical, laser, ion etc etc, and each of these gun types needs to react differently to different types of armour, such as physical, shields, reflective coatings, lead paint etc etc. So you have started doing your code and you have come to the point where you are flying around shooting things and you notice that your ion cannon is doing 0 damage to a ship... so you go to you re-start, faff around setting up your debugger to run, equip your ship with the same items and fly off to find the closest other ship to shoot. You hit it and it does 0 damage so you trace through the stack working out at which point the calculation went wrong, then you realise you made a typo and instead of doing 0.5f you did 0.
Not the end of the world, but this sort of thing could be caught earlier on in your development cycle, before you even run your game. Also writing your code in a way which lets it be tested makes your code better, you instantly write more separated and easier to maintain code and you can also then benefit from dependency injection techniques if you write your objects in such a way that they can be mocked. (If you don't use Dependency Injection you should look into it, IoC is best, ServiceLocator is bare minimum)
So how do we go about doing this? As depending on your engine you may approach this problem different ways, so in Unity you may have written this bullet firing logic as a script and attached it to a weapon so it is a MonoBehaviour (this makes it almost impossible to unit test properly and we will get on to that later), or if you have your own engine it may be some fancy pants private method put into your ship object or something, or you may have gone for something all together different.
However if we look at the bare bones of what we are trying to achieve here, we have a Weapon which has an ammo type, a damage amount then we have some armour which has a defence type and an armour amount (being very simple here). So rather than making these parts of another class lets model these as they should be done in code (I will use C# to demo).
public class Weapon
{
byte DamageType { get; set; }
float Damage {get; set;}
}
public class Armour
{
byte DefenseType { get; set; }
float Armour { get; set; }
}
So those seem legit, they do the bare minimum and have no knowledge of a ship, space or have any logic applied... now some of you will rush in and add a method to weapon or armour to handle the interaction between the 2, but really it should be another entity entirely that handles the damage between the 2, as these are just models of the LOGICAL REPRESENTATION OF A GAME ENTITY. Those letters are in caps because they are very important. If you are going to write good code you should split the LOGICAL representation of your entities from their PHYSICAL/RENDERED representations, this way you do not even need to render things to test your game, and if you dont need to render things you dont really need to run your game either... you could do it all from a console app OR A UNIT TEST RUNNER!
So this is NUnit's GUI test runner, it has a console test runner (or if you are super cool you will own Resharper for Visual Studio or Rider which is far better).
So with this you can write some isolated tests, to prove that what you THINK happens in the logic, will actually happen, and you can run these tests every time you re-build your code, you check in your source control, or if you are super swanky you may have a build server and pipeline which you can plug it into.
Anyway so lets get on with a very simple implementation of a component to handle the interaction between weapon and armour so we can do a dummy test:
public class DamageCalculator : IDamageCalculator // You may have different implementations
{
float CalculateDamage(Weapon weapon, Armour armour)
{ return weapon.Damage - armour.Armour; }
}
Now that doesn't take into account the type of armour or damage, but if you imagine your implementation would have that stuff in. You can clearly see here that if we ignore the rendered stuff at the heart of the interaction where one entity attacks another, there will be resulting damage which can be applied to the victim, or could be passed over to some other entity to handle to check for shield reduction and stuff. However the key is that this is isolated logic, it is not tied to some mono behaviour, it is not tied to some inheritance chain, it is something very simple that encapsulates a single responsibility.
So how do we test this?
[TestFixture]
public class DamageCalculatorTests
{
[Test]
public void should_correctly_calculate_basic_weapon_damage()
{
var weapon = new Weapon { Damage = 10 };
var armour = new Armour { Armour = 5 };
var damageCalculator = new DamageCalculator();
var expectedResult = 5;
var result = damageCalculator.CalculateDamage(weapon, armour);
Assert.That(result, Is.EqualTo(expectedResult));
// This will pass
}
[Test]
public void should_correctly_calculate_ion_damage_vs_shield_defense()
{
var weapon = new Weapon { DamageType = 2, Damage = 5.5f };
var armour = new Armour { ArmourType = 3, Armour = 2 };
var damageCalculator = new DamageCalculator();
var expectedResult = 1.5f; // Lets pretend our game design document said that shields are x2 defense against ion damage (5.5f - (2*2))
var result = damageCalculator.CalculateDamage(weapon, armour);
// This will fail as the actual result with our current implementation is gonna be 3.5f
}
}
So this above test fixture (what NUnit calls a class of testable methods) contains 2 tests, one which passes and one which fails. If you run this through the test runner (go google how) then you would be able to see without even running your game that this logic is not 100% right, and whats better you can then refactor your logic to make it pass the second test. This can be run as often as you like, so if you get some new guy on the team who decides to go re-write the weapon logic, does it still adhere to the game design requirements, and this is where those requirements mentioned earlier on really come into play, as if you have requirements like:
Implement Ion Damage Logic
As a designer
I want to have ion type weapons
So that I am stronger against physical armour
And weaker against shield armour
Given I have a weapon which does 5.5 ion damage
And the opponent has shield type armour of 2
When I attack
Then I should do 1.5 damage
Ok maybe you dont need to be explicit with the numbers and I am not sure if Ion is meant to have the above properties... but if you were to write a task like the above you can easily see if the developer has succeeded in doing what you wanted. So that new guy who has re-written all your logic may have buggered up your Plasma type damage but without a suite of unit tests to prove that this stuff works you wont know until you manually play the game and find it out. So this can REALLY SAVE TIME in the long run.
This doesn't mean you don't need testers, you still need them but they would do more exploratory testing to make sure the game functions and feels right, not having to go off shooting every type of weapon against every kind of ship every time a new release is made (regression testing).
So save yourself time and resources, and design your code well from the get go and separate your LOGIC from your PHYSICAL entities, I wont go over mocking here as I would need to make a contrived example with a player, but this is enough to get you started down that road. If you were to do this in unity you can have all your logic in another project and just put your logic dlls into your unity project and add the logical representations to your physical representations at runtime, so your monobehaviour now contains a Weapon class as a property, not as a hardcoded method within itself.