4
\$\begingroup\$

I'm new to unit testing, and to learn, I've decided to "contribute" to Monogame to help with their testing efforts. The issue at hand is to test the input device classes. I've started with the GamePad class.

Here are the tests that I have implemented.

namespace MonoGame.Tests
{
    [TestFixture]
    public class GamePadTests
    {
        private Array GetPlayerIndexes() // gets a list of enum values to test
        {
            return Enum.GetValues(typeof(PlayerIndex));
        }

        private Array GetGamePadDeadZoneIndexes()
        {
            return Enum.GetValues(typeof(GamePadDeadZone));
        }

        [Test] // generate tests for each PlayerIndex enum value
        public void GetCapabilities_ShouldNotReturnNull_GivenPlayerIndex([ValueSource("GetPlayerIndexes")] PlayerIndex pi)
        {
            Assert.IsNotNull(GamePad.GetCapabilities(pi));
        }

        [Test] // generate tests for each PlayerIndex enum value
        public void GetCapabilities_ShouldReturnGamePadCapabilities_GivenPlayerIndex([ValueSource("GetPlayerIndexes")] PlayerIndex pi)
        {
            Assert.IsInstanceOf<GamePadCapabilities>(GamePad.GetCapabilities(pi));
        }

        [Test]
        public void GetCapabilies_ShouldNotReturnNull_GivenInt([Range(-1, 4)] int i) // range to test negative, PlayerIndex, and higher values
        {
            Assert.IsInstanceOf<GamePadCapabilities>(GamePad.GetCapabilities(i));
        }

        [Test]
        public void GetCapabilities_ShouldReturnGamePadCapabilities_WhenGivenInt([Range(-1, 4)] int i) // range to test negative, PlayerIndex, and higher values
        {
            Assert.IsInstanceOf<GamePadCapabilities>(GamePad.GetCapabilities(i));
        }

        [Test] // generate tests for each PlayerIndex enum value
        public void GetState_ShouldNotReturnNull_WhenGivenPlayerIndex([ValueSource("GetPlayerIndexes")] PlayerIndex pi)
        {
            Assert.IsNotNull(GamePad.GetState(pi));
        }

        [Test] // generate tests for each PlayerIndex enum value
        public void GetState_ShouldReturnGamePadState_WhenGivenPlayerIndex([ValueSource("GetPlayerIndexes")] PlayerIndex pi)
        {
            Assert.IsInstanceOf<GamePadState>(GamePad.GetState(pi));
        }

        [Test]
        public void GetState_ShouldNotReturnNull_WhenGivenInt([Range(-1, 4)] int i) // range to test negative, PlayerIndex, and higher values
        {
            Assert.IsNotNull(GamePad.GetState(i));
        }
        [Test]
        public void GetState_ShouldReturnGamePadState_WheGivenInt([Range(-1, 4)] int i) // range to test negative, PlayerIndex, and higher values
        {
            Assert.IsInstanceOf<GamePadState>(GamePad.GetState(i));
        }

        [Test]
        public void GetState_ShouldNotReturnNull_WhenGivenPlayerIndex_GamePadDeadZone(
            [ValueSource("GetPlayerIndexes")] PlayerIndex pi, // generate tests for each combination of PlayerIndex and GamePadDeadZone enum values
            [ValueSource("GetGamePadDeadZoneIndexes")] GamePadDeadZone gpdz
            )
        {
            Assert.IsNotNull(GamePad.GetState(pi, gpdz));
        }

        [Test]
        public void GetState_ShouldReturnGamePadState_WhenGivenPlayerIndex_GamePadDeadZone(
            [ValueSource("GetPlayerIndexes")] PlayerIndex pi, // generate tests for each combination of PlayerIndex and GamePadDeadZone enum values
            [ValueSource("GetGamePadDeadZoneIndexes")] GamePadDeadZone gpdz
            )
        {
            Assert.IsInstanceOf<GamePadState>(GamePad.GetState(pi, gpdz));
        }

        [Test]
        public void GetState_ShouldNotReturnNull_WhenGivenInt_GamePadDeadZone(
            [Range(-1, 4)] int i, // generate tests for each combination of the index range and GamePadDeadZone enum values
            [ValueSource("GetGamePadDeadZoneIndexes")] GamePadDeadZone gpdz
            )
        {
            Assert.IsNotNull(GamePad.GetState(i, gpdz));
        }

        [Test]
        public void GetState_ShouldReturnGamePadState_WhenGivenInt_GamePadeDeadZone(
            [Range(-1, 4)] int i, // generate tests for each combination of the index range and GamePadDeadZone enum values
            [ValueSource("GetGamePadDeadZoneIndexes")] GamePadDeadZone gpdz
            )
        {
            Assert.IsInstanceOf<GamePadState>(GamePad.GetState(i, gpdz));
        }

        [Test]
        public void SetVibration_ShouldNotReturnNull_WhenGivenPlayerIndex_Float_Float(
            [ValueSource("GetPlayerIndexes")] PlayerIndex pi, // generate tests for each combination of the PlayerIndex enum values and both float ranges
            [Range(0.0f, 1.0f, 0.25f)] float lm,
            [Range(0.0f, 1.0f, 0.25f)] float rm
            )
        {
            Assert.IsNotNull(GamePad.SetVibration(pi, lm, rm));
        }

        [Test]
        public void SetVibration_ShouldReturnBool_WhenGivenPlayerIndex_Float_Float(
            [ValueSource("GetPlayerIndexes")] PlayerIndex pi, // generate tests for each combination of the PlayerIndex enum values and both float ranges
            [Range(0.0f, 1.0f, 1.0f)] float lm,
            [Range(0.0f, 1.0f, 1.0f)] float rm
            )
        {
            Assert.IsInstanceOf<bool>(GamePad.SetVibration(pi, lm, rm));
        }

        [Test]
        public void MaximumGamePadCount_ShouldNotReturnNull_WhenGet()
        {
            Assert.IsNotNull(GamePad.MaximumGamePadCount);
        }

        [Test]
        public void MaximumGamePadCount_ShouldReturnInt_WhenGet()
        {
            Assert.IsInstanceOf<int>(GamePad.MaximumGamePadCount);
        }
    }
}

For your information, here's the code under test, the GamePad class, and this is not to be reviewed, please consider it as given:

namespace Microsoft.Xna.Framework.Input
{
    /// <summary> 
    /// Supports querying the game controllers and setting the vibration motors.
    /// </summary>
    public static partial class GamePad
    {
        /// <summary>
        /// Returns the capabilites of the connected controller.
        /// </summary>
        /// <param name="playerIndex">Player index for the controller you want to query.</param>
        /// <returns>The capabilites of the controller.</returns>
        public static GamePadCapabilities GetCapabilities(PlayerIndex playerIndex)
        {
            return GetCapabilities((int)playerIndex);
        }

        /// <summary>
        /// Returns the capabilites of the connected controller.
        /// </summary>
        /// <param name="index">Index for the controller you want to query.</param>
        /// <returns>The capabilites of the controller.</returns>
        public static GamePadCapabilities GetCapabilities(int index)
        {
            if (index < 0 || index >= PlatformGetMaxNumberOfGamePads())
                return new GamePadCapabilities();

            return PlatformGetCapabilities(index);
        }

        /// <summary>
        /// Gets the current state of a game pad controller with an independent axes dead zone.
        /// </summary>
        /// <param name="playerIndex">Player index for the controller you want to query.</param>
        /// <returns>The state of the controller.</returns>
        public static GamePadState GetState(PlayerIndex playerIndex)
        {
            return GetState((int)playerIndex, GamePadDeadZone.IndependentAxes);
        }

        /// <summary>
        /// Gets the current state of a game pad controller with an independent axes dead zone.
        /// </summary>
        /// <param name="index">Index for the controller you want to query.</param>
        /// <returns>The state of the controller.</returns>
        public static GamePadState GetState(int index)
        {
            return GetState(index, GamePadDeadZone.IndependentAxes);
        }

        /// <summary>
        /// Gets the current state of a game pad controller, using a specified dead zone
        /// on analog stick positions.
        /// </summary>
        /// <param name="playerIndex">Player index for the controller you want to query.</param>
        /// <param name="deadZoneMode">Enumerated value that specifies what dead zone type to use.</param>
        /// <returns>The state of the controller.</returns>
        public static GamePadState GetState(PlayerIndex playerIndex, GamePadDeadZone deadZoneMode)
        {
            return GetState((int)playerIndex, deadZoneMode);
        }

        /// <summary>
        /// Gets the current state of a game pad controller, using a specified dead zone
        /// on analog stick positions.
        /// </summary>
        /// <param name="index">Index for the controller you want to query.</param>
        /// <param name="deadZoneMode">Enumerated value that specifies what dead zone type to use.</param>
        /// <returns>The state of the controller.</returns>
        public static GamePadState GetState(int index, GamePadDeadZone deadZoneMode)
        {
            if (index < 0 || index >= PlatformGetMaxNumberOfGamePads())
                return GamePadState.Default;

            return PlatformGetState(index, deadZoneMode);
        }

        /// <summary>
        /// Sets the vibration motor speeds on the controller device if supported.
        /// </summary>
        /// <param name="playerIndex">Player index that identifies the controller to set.</param>
        /// <param name="leftMotor">The speed of the left motor, between 0.0 and 1.0. This motor is a low-frequency motor.</param>
        /// <param name="rightMotor">The speed of the right motor, between 0.0 and 1.0. This motor is a high-frequency motor.</param>
        /// <returns>Returns true if the vibration motors were set.</returns>
        public static bool SetVibration(PlayerIndex playerIndex, float leftMotor, float rightMotor)
        {
            return SetVibration((int)playerIndex, leftMotor, rightMotor);
        }

        /// <summary>
        /// Sets the vibration motor speeds on the controller device if supported.
        /// </summary>
        /// <param name="index">Index for the controller you want to query.</param>
        /// <param name="leftMotor">The speed of the left motor, between 0.0 and 1.0. This motor is a low-frequency motor.</param>
        /// <param name="rightMotor">The speed of the right motor, between 0.0 and 1.0. This motor is a high-frequency motor.</param>
        /// <returns>Returns true if the vibration motors were set.</returns>
        public static bool SetVibration(int index, float leftMotor, float rightMotor)
        {
            if (index < 0 || index >= PlatformGetMaxNumberOfGamePads())
                return false;

            return PlatformSetVibration(index, MathHelper.Clamp(leftMotor, 0.0f, 1.0f), MathHelper.Clamp(rightMotor, 0.0f, 1.0f));
        }

        /// <summary>
        /// The maximum number of game pads supported on this system.  Attempting to
        /// access a gamepad index higher than this number will result in an <see cref="InvalidOperationException"/>
        /// being thrown by the API.
        /// </summary>
        public static int MaximumGamePadCount
        {
            get { return PlatformGetMaxNumberOfGamePads(); }
        }
    }
}

The problem at hand is that I'm not sure that I've actually done anything other some null and type checking and "interface verification". I spoke with someone on the project and they said that I'm generally not allowed to mock or change other code to aid in testing. Not that I know much about mocking particularly. I just don't know if I can do any more substantial testing to this class.

Is this about the expected form for unit testing? Is there more I can do to test this class in general or improve my code?

\$\endgroup\$
1
\$\begingroup\$

Your tests as they stand aren't great, they are probably adding friction rather than value. That said, it depends a bit on what you're trying to get out of testing in this manner.

As an example:

[Test]
public void MaximumGamePadCount_ShouldNotReturnNull_WhenGet()
{
    Assert.IsNotNull(GamePad.MaximumGamePadCount);
}

[Test]
public void MaximumGamePadCount_ShouldReturnInt_WhenGet()
{
    Assert.IsInstanceOf<int>(GamePad.MaximumGamePadCount);
}

These to tests are testing this property:

public static int MaximumGamePadCount { get { return PlatformGetMaxNumberOfGamePads(); } }

As it stands, the property can only ever return an int, that isn't null, this is enforced by the compiler. All your tests do is make sure that the type of the property can't be changed. In rare cases you might want to have tests like this (for example if you had a versioned API and you wanted to ensure that changes didn't impact released versions), but generally you want to be testing the behaviour or methods, not the implementation.

Without looking at more of the production code (which you haven't posted) it's hard to say what approach you should taking to writing tests. The GamePad class is partial, which suggests there's other implementation elements somewhere.

Looking at this method:

public static GamePadCapabilities GetCapabilities(int index)
{
    if (index < 0 || index >= PlatformGetMaxNumberOfGamePads())
        return new GamePadCapabilities();

    return PlatformGetCapabilities(index);
}

You have these tests:

[Test]
public void GetCapabilies_ShouldNotReturnNull_GivenInt([Range(-1, 4)] int i) // range to test negative, PlayerIndex, and higher values
{
    Assert.IsInstanceOf<GamePadCapabilities>(GamePad.GetCapabilities(i));
}

[Test]
public void GetCapabilities_ShouldReturnGamePadCapabilities_WhenGivenInt([Range(-1, 4)] int i) // range to test negative, PlayerIndex, and higher values
{
    Assert.IsInstanceOf<GamePadCapabilities>(GamePad.GetCapabilities(i));
}

Both tests simply verify that an instance of GamePadCapabilities is returned. Really you should be looking what the method does and testing for that if possible. It seems like for some indexes it returns a new GamePadCapabilities whilst for others it returns PlatformGetCapabilities(index). What's the differences between these two returned objects? Are the methods being called public or private? It could be that testing that calling PlatformGetCapabilities(index) from your test returns that same instance as calling GetCapabilities for certain indexes is a sensible test. Or maybe it's calling GetCapabilities with a known index always returns an expected list of capabilities.

You've said modifying production code to make it more testable and using mocking is a no-no. How are other classes within the system tested? This should be a good indicator of the level of testing expected for your project.

\$\endgroup\$
  • \$\begingroup\$ The full source is linked at the top. The project extends xna which was specific to certain platforms to other platforms, so there are existing classes and extensions for new platforms. In this case, platform* denotes the platform specific implementation as I understand it. This is part of a larger push to cover a previously untested framework. I also received that feedback about null checking int in my pull request and have since removed it along with some other changes. You comment about checking the objects against each other is interesting. I will see if it is applicable. \$\endgroup\$ – Corey Thompson May 23 '16 at 6:39

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.