How to test a dynamically list properly

Viewed 47

I want to test that a bunch of buttons on screen are responding properly, this amount this increase dynamically thought the project and will be used in more than a single test.

  1. My first try was using [TestCase] attribute

     [TestCase("High action - Button")]
     [TestCase("Low action - Button")]
     [TestCase("Right action - Button")]
     [TestCase("Left action - Button")]
     public void WhenClickOnActionButtons_ActionStreamShouldIncrease (string buttonNameInScene)
    

But this means that every time that some buttons were added I would need to remember to come back to every test that use the buttons and manually add the new test case

  1. Then I thought that have a container that control the buttons would solve my problem, I will lost the possibility to see which buttons caused the error in the test suite, but this can be fixed just by adding a simple message into the assert

     [Test]
     [Order(1)]
     public IEnumerator WhenClickOnActionButtons_ActionStreamShouldIncrease ()
     {
         foreach (var battleActionButton in battleActionButtons) // <- Buttons containers
         {
             // Arrange
             var changedWhenClicked = false;
             battleActionButton.GetComponent<Button>().onClick.AddListener(() => changedWhenClicked = true);
    
             // Act
             var positionOfButtonInScreen = Camera.main.WorldToScreenPoint(battleActionButton.transform.position);
             LeftClickOnScreenPosition(positionOfButtonInScreen);
             yield return null;
    
             // Assert
             Assert.True(changedWhenClicked, $"{battleActionButton.name} didn't pressed correctly");
         }
     }
    

But now if the button container is empty the test pass, so I thought, let's create a test that runs before it to check if the container is empty

    [Test]
    [Order(0)]
    public void ActionButtonsGroup_IsGreaterThan0()
    {
        // Arrange
        // Act
        // Assert
        Assert.That(battleActionButtons.Count, Is.GreaterThan(0));
    }

But then, if empty, this fails, and the next pass, so I thought, maybe I could make the tests stop running when a test fails, but then I discovered that a test should not rely on other tests, so my question is, how should I handle this kindle of case?

2 Answers

You can put the assertion from the second test before the foreach loop:

[Test]
[Order(1)]
public IEnumerator WhenClickOnActionButtons_ActionStreamShouldIncrease ()
{
    // Precondition
    Assert.That(battleActionButtons.Count, Is.GreaterThan(0));
    foreach (var battleActionButton in battleActionButtons) // <- Buttons containers
    {
        // Arrange
        var changedWhenClicked = false;
        battleActionButton.GetComponent<Button>().onClick.AddListener(() => changedWhenClicked = true);

        // Act
        var positionOfButtonInScreen = Camera.main.WorldToScreenPoint(battleActionButton.transform.position);
        LeftClickOnScreenPosition(positionOfButtonInScreen);
        yield return null;

        // Assert
        Assert.True(changedWhenClicked, $"{battleActionButton.name} didn't pressed correctly");
    }
}

This sounds like a case for using a proper collection as a data source for a parametrised test. In NUnit you can use TestCaseSource for this:

[TestCaseSource(typeof(BattleActionButtons))]
public void ButtonTest(BattleActionButton button)
{
    // Test body goes here...
}

where BattleActionButtons is a real class:

public sealed class BattleActionButtons : IReadOnlyCollection<BattleActionButton>
{
    private readonly List<BattleActionButton> buttons = new();

    public BattleActionButtons()
    {
        // Add buttons here...
    }

    public int Count => buttons.Count;

    public IEnumerator<BattleActionButton> GetEnumerator()
    {
        return buttons.GetEnumerator();
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }
}

Perhaps BattleActionButtons is just a test-specific class, but perhaps it would make sense in the production code as well..(?) With TDD, it often happens that you introduce an abstraction to factor your tests in a nice way, only to discover that this abstraction is of general usefulness.

If you're concerned that BattleActionButtons is empty, write another test that verifies that this isn't the case:

[Test]
public void ButtonsAreNotEmpty()
{
    Assert.IsNotEmpty(new BattleActionButtons());
}

There's no need to order the tests. If BattleActionButtons is empty, ButtonsAreNotEmpty will fail. It's true that the data-driven parametrised test (here called ButtonTest) will pass when the collection is empty, but there's nothing wrong in that. It just becomes a vacuous truth.

(Unit tests are essentially predicates, and a parametrised test is a universal claim that states that for all (∀) values in the data source, the predicate holds. Thus, when the data source is empty, any test is trivially going to pass, which is exactly as it should be according to predicate logic.)

Related