1
\$\begingroup\$

This is my second attempt with unit-tests. Like my previous attempt, I would prefer a focus on the unit-tests, however I am always happy to improve my code in any way reviewers can help.

Once again it is just a single class along with supporting unit-tests. The class is named UpgradeButton and represents upgrades that the user can purchase in the game.

The button still looks like this:

Rectangle button with mouse over it and a green and red indicator

The boxes along the bottom are updated programatically through the input() and update() functions that indicate what level you are at and what you can purchase. The number of tiers is assigned in the constructor, as is the label.

Expressions.h is just a header of constexpr for replacing magic numbers, and as you can see I am using SFML. Version 2.4.2 if it matters. I'd also like to point out that I am aware of the difference between #pragma once and include guards. I chose the former because they are less verbose and supported by the major compilers. Lastly, it is a small personal project, completely in my control.

UpgradeButton.h

#pragma once

#include "Expressions.h"

#include <Graphics.hpp>

#include <string>

namespace fleet {
    class UpgradeButton : public sf::Drawable {
    public:
        explicit UpgradeButton(const std::string& newLabel, const sf::Font& font, unsigned short numUpgrades);

        void setPosition(float x, float y);
        void setPosition(const sf::Vector2f& position);
        void setLabelString(const std::string& string);
        void setCharacterSize(unsigned newSize);
        void setTextFillColor(const sf::Color& color);

        const sf::Vector2f& getPosition() const { return button.getPosition(); }
        const sf::String& getLabelString() const { return label.getString(); }
        unsigned getCharacterSize() const { return label.getCharacterSize(); }
        const sf::Color& getTextFillColor() const { return label.getFillColor(); }
        const std::vector<sf::RectangleShape>& getIndicators() const { return indicators; }

        bool input(const sf::Vector2f& mousePos, bool canAfford);
        void update(const sf::Vector2f& mousePos, unsigned currentLevel, bool canAfford);
    private:
        sf::RectangleShape button{ sf::Vector2f(default_upgrade_width, default_upgrade_height) };
        sf::Text label;
        std::vector<sf::RectangleShape> indicators;

        void draw(sf::RenderTarget& target, sf::RenderStates states) const override;
    };
}

UpgradeButton.cpp

#include "UpgradeButton.h"

#include "Exceptions.h"

namespace fleet {
    UpgradeButton::UpgradeButton(const std::string& newLabel, const sf::Font& font, unsigned short numUpgrades) :
        label{ newLabel, font }
    {
        if (numUpgrades == 0) {
            throw DivideByZeroException("Attempted to divide by zero in the constructor of UpgradeButton");
        }
        float indicatorWidth = default_upgrade_width / numUpgrades;
        for (unsigned i = 0; i < numUpgrades; ++i) {
            indicators.emplace_back(sf::RectangleShape(sf::Vector2f(indicatorWidth, default_indicator_height)));
        }

        button.setFillColor(sf::Color::Cyan);
        button.setOutlineThickness(upgrade_button_outline);
        for (auto& indicator : indicators) {
            indicator.setFillColor(sf::Color::Cyan);
            indicator.setOutlineThickness(upgrade_button_outline);
        }
    }

    // UpgradeButton requires one call to setPosition minimum to properly initialize indicators
    // in there proper position. Failure to do so will leave them all positioned at 0, 0;
    void UpgradeButton::setPosition(float x, float y)
    {
        button.setPosition(x, y);
        float indicatorX = x;
        float indicatorY = y + (default_upgrade_height - default_indicator_height);
        for (auto& indicator : indicators) {
            indicator.setPosition(indicatorX, indicatorY);
            indicatorX += indicator.getSize().x;
        }
        float labelY = y + default_upgrade_height + upgrade_label_y_offset;
        label.setPosition(x, labelY);
    }
    void UpgradeButton::setPosition(const sf::Vector2f& position)
    {
        setPosition(position.x, position.y);
    }
    void UpgradeButton::setLabelString(const std::string& string)
    {
        label.setString(string);
    }
    void UpgradeButton::setCharacterSize(unsigned newSize)
    {
        label.setCharacterSize(newSize);
    }
    void UpgradeButton::setTextFillColor(const sf::Color& color)
    {
        label.setFillColor(color);
    }

    bool UpgradeButton::input(const sf::Vector2f& mousePos, bool canAfford)
    {
        return canAfford && button.getGlobalBounds().contains(mousePos);
    }
    void UpgradeButton::update(const sf::Vector2f& mousePos, unsigned currentLevel, bool canAfford)
    {
        for (unsigned i = 0; i < currentLevel && i < indicators.size(); ++i) {
            indicators[i].setFillColor(sf::Color::Green);
        }
        for (unsigned i = currentLevel; i < indicators.size(); ++i) {
            indicators[i].setFillColor(sf::Color::Cyan);
        }

        if (currentLevel >= indicators.size()) { return; }

        if (button.getGlobalBounds().contains(mousePos)) {
            if (canAfford) {
                indicators[currentLevel].setFillColor(sf::Color::Green);
            }
            else {
                indicators[currentLevel].setFillColor(sf::Color::Red);
            }
        }
        else {
            indicators[currentLevel].setFillColor(sf::Color::Cyan);
        }
    }

    void UpgradeButton::draw(sf::RenderTarget& target, sf::RenderStates states) const
    {
        target.draw(button, states);
        for (auto& indicator : indicators) {
            target.draw(indicator, states);
        }
        target.draw(label, states);
    }
}

As for the tests. I am using Google Test. I got rid of the tests of primitive setter methods. They always felt exceedingly trivial. I tried to recognize the assumptions I make in my code and then test for it, but I have found that isn't as easy as it sounds. There are currently nine tests, and they all pass.

UpgradeButtonTests.cpp

#include "gtest/gtest.h"

#include "UpgradeButton.h"

#include "Exceptions.h"
#include "Expressions.h"

#include <Graphics.hpp>

namespace fleet {
    class UpgradeButtonTest : public testing::Test {
    protected:
        UpgradeButton* button;
    };

    TEST_F(UpgradeButtonTest, constructor_accepts_empty_string) {
        button = new UpgradeButton("", sf::Font(), 5);
        ASSERT_STREQ("", button->getLabelString().toAnsiString().c_str());
        delete button;
    }

    TEST_F(UpgradeButtonTest, constructor_handles_large_strings) {
        std::string longString = "This is a very long string to test that the constructor of the \
            UpgradeButton class properly handles very long strings.";
        button = new UpgradeButton(longString, sf::Font(), 5);
        ASSERT_EQ(longString, button->getLabelString());
    }

    TEST_F(UpgradeButtonTest, constructor_fails_on_zero) {
        ASSERT_THROW(button = new UpgradeButton("Test String", sf::Font(), 0), DivideByZeroException);
    }

    TEST_F(UpgradeButtonTest, text_is_white_on_contruction) {
        button = new UpgradeButton("Test String", sf::Font(), 5);
        ASSERT_EQ(sf::Color::White, button->getTextFillColor());
    }

    TEST_F(UpgradeButtonTest, indicators_stacked_at_origin_point_on_construction) {
        button = new UpgradeButton("Test String", sf::Font(), 5);
        for (auto& indicator : button->getIndicators())
        {
            ASSERT_EQ(sf::Vector2f(), indicator.getPosition());
        }
    }

    TEST_F(UpgradeButtonTest, setPosition_sets_indicators_correctly) {
        unsigned short numIndicators = 5;
        button = new UpgradeButton("Test String", sf::Font(), numIndicators);
        sf::Vector2f position(5.F, 5.F);
        button->setPosition(position);
        float indicatorX = position.x;
        float indicatorY = position.y + (default_upgrade_height - default_indicator_height);
        for (auto& indicator : button->getIndicators()) {
            ASSERT_FLOAT_EQ(indicatorX, indicator.getPosition().x);
            ASSERT_FLOAT_EQ(indicatorY, indicator.getPosition().y);
            indicatorX += indicator.getSize().x;
        }


        position = sf::Vector2f(indicatorX, indicatorY);
        button->setPosition(position);
        indicatorY = position.y + (default_upgrade_height - default_indicator_height);
        for (auto& indicator : button->getIndicators()) {
            ASSERT_FLOAT_EQ(indicatorX, indicator.getPosition().x);
            ASSERT_FLOAT_EQ(indicatorY, indicator.getPosition().y);
            indicatorX += indicator.getSize().x;
        }
    }

    TEST_F(UpgradeButtonTest, input) {
        button = new UpgradeButton("Test String", sf::Font(), 5);
        ASSERT_TRUE(button->input(sf::Vector2f(), true));
        ASSERT_FALSE(button->input(sf::Vector2f(), false));
        ASSERT_FALSE(button->input(sf::Vector2f(500.F, 900.F), true));
        ASSERT_FALSE(button->input(sf::Vector2f(500.F, 900.F), false));
    }

    TEST_F(UpgradeButtonTest, update_does_not_fail_out_of_range) {
        unsigned short numIndicators = 5;
        button = new UpgradeButton("Test String", sf::Font(), numIndicators);
        numIndicators += 7;
        ASSERT_NO_THROW(button->update(sf::Vector2f(), numIndicators, true));
    }

    TEST_F(UpgradeButtonTest, update_handles_mousePos_correctly) {
        unsigned short numIndicators = 5;
        button = new UpgradeButton("Test String", sf::Font(), numIndicators);
        button->update(sf::Vector2f(-10.F, -10.F), 0, true);
        for (auto& indicator : button->getIndicators()) {
            ASSERT_EQ(sf::Color::Cyan, indicator.getFillColor());
        }
        button->update(sf::Vector2f(), 0, true);
        ASSERT_EQ(sf::Color::Green, button->getIndicators()[0].getFillColor());
    }
}
\$\endgroup\$
  • \$\begingroup\$ I think there is a small typo in the comment for the setPosition function (there/their). \$\endgroup\$ – yuri Apr 28 at 11:49
  • \$\begingroup\$ @yuri good catch thanks. I fixed it in my repo but not here as it is a trivial edit. If I find another reason to edit the post hopefully I remember to fix that as well. \$\endgroup\$ – bruglesco Apr 28 at 12:21

Your Answer

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

Browse other questions tagged or ask your own question.