using System; using System.Collections; using NUnit.Framework; using UnityEngine; using UnityEngine.SceneManagement; using UnityEngine.TestTools; namespace Tests { public class ShopUnitTests { private ShopView shopView; //This is the grid buy view we want to test private SellViewGrid sellView; private ShopView upgradeView; //Setup the test scene [OneTimeSetUp] public void LoadShopScene() { // Load the Scene to do unit test. In the scope of this project, this is fine. In a more complicated project, a game scene could take // a long time to load, in which case it's better to create test scenes to do unit tests SceneManager.LoadScene(0); } //Setup the unit tests here [UnitySetUp] public IEnumerator SetupTests() { yield return null; //yield return null skips one frame, this is to make sure that this happens after the scene is loaded //The shop scene only contains one grid buy view, we use Resources.FindObjectsOfTypeAll to get the reference to it, //Resources.FFindObjectsOfTypeAll is used instead of GameObject.Find because the later can't find disabled objects var shops = Resources.FindObjectsOfTypeAll(); shopView = shops[2]; // For some reason this reverses it. We just want to hardcode it for this scene, no annoying for loops to compare upgradeView = shops[1]; sellView = Resources.FindObjectsOfTypeAll()[0]; // Turn on all views so they function correctly. We do not care for the looks during unit tests, obviously. sellView.gameObject.SetActive(true); upgradeView.gameObject.SetActive(true); //Active the gridBuyView game object to initialize the class, if we don't do this 'void Start()' won't be called //You should active all the game objects that are involved in the test before testing the functions from their components shopView.gameObject.SetActive(true); } // Use meaningful name for your test cases, this case tests if the ShopGridBuyView component has initialized its ShopModel property [UnityTest] public IEnumerator ShopGridBuyViewInitializedShopModel() { yield return null; //yield return null skips one frame, waits for the Unity scene to load //now test if a ShopModel is assigned to gridBuyView Assert.IsNotNull(shopView.ShopModel, "No Model is assigned in ShopView"); } //This case tests if the grid buy view displays the correct amount of Items [UnityTest,Order(1)] // Run this one first to see if we populated everything correctly public IEnumerator ShopGridBuyViewDisplaysCorrectAmountOfItems() { yield return null; //yield return null skips one frame, waits for the Unity scene to load //Now that the scene is loaded and the gridBuyView game object was activated in SetupTests(), we can use GameObject.Find //to find the game object we want to test GameObject gridItemsPanel = GameObject.Find("GridItemsPanel"); yield return new WaitForEndOfFrame(); //Since we are testing how many items are displayed, we should use WaitForEndOfFrame to wait until the end of the frame, //so that the view finished updating and rendering everything yield return null; int itemCount = gridItemsPanel.transform.childCount; Assert.AreEqual(shopView.ShopModel.inventory.GetItemCount(), itemCount, "The generated item count is not equal to shopModel's itemCount"); } // This case tests if the shop model gets populated with items [UnityTest,Order(0)] // Run this one before the view tests to see if the factory works public IEnumerator ShopFactoryPopulatesShop() { yield return null; //yield return null skips one frame, waits for the Unity scene to load //Now that the scene is loaded and the gridBuyView game object was activated in SetupTests(), we can use GameObject.Find //to find the game object we want to test var count = 0; Assert.DoesNotThrow(delegate { count = shopView.ShopModel.inventory.GetItemCount(); }); Assert.Greater(count,1); // Having just one item probably means something went wrong and crashed } //This case tests if the buyModel can throw an ArgumentOutOfRangeException when it's asked to select an item by a negative //index. Incorrect indexes can be generated from bugs in views or controllers, throwing the correct type of exceptions is //better than failing silently for debugging. Your unit tests should cover exception handlings [UnityTest] public IEnumerator ShopModelThrowsExceptionsWhenSelectingNegativeIndex() { //yield return null skips one frame, waits for the Unity scene to load and buyModel to be assigned yield return null; //Creates a delegate that call gridBuyView.ShopModel.SelectItemByIndex(-1), the test runner will run the function, and //check if an ArgumentOutOfRangeException is thrown, the unit test would fail if no ArgumentOutOfRangeException //was thrown Assert.Throws(delegate { shopView.ShopModel.SelectItemByIndex(-1); }); } //This case tests whether info panels and selection highlights work correctly when a specific item is selected, while disabling correctly when it is unselected [UnityTest] public IEnumerator UpdateViewItemInfoPanelAndSelection() { yield return null; var selectedItem = shopView.transform.Find("GridItemsPanel"); var active = selectedItem.GetComponentInChildren(); // In grid view, one item panel will be active. That's the selected item! shopView.ShopModel.SelectItemByIndex(shopView.ShopModel.GetSelectedItemIndex() + 1); var newActive = selectedItem.GetComponentInChildren(); Assert.AreNotSame(active, newActive,"Previous item is still selected!"); } // This test case tests that either no item is selected, or exactly one item is selected [UnityTest] public IEnumerator NoMoreThanOneItemSelectedInView() { yield return null; var selectedItem = shopView.transform.Find("GridItemsPanel"); var active = selectedItem.GetComponentsInChildren(); // In grid view, one item panel will be active. That's the selected item! Assert.Greater(2,active.Length,"Too many items selected!"); // Depending on what's up, either we want none or just one selected now shopView.ShopModel.SelectItemByIndex(shopView.ShopModel.GetSelectedItemIndex() + 1); shopView.ShopModel.SelectItemByIndex(shopView.ShopModel.GetSelectedItemIndex() + 1); active = selectedItem.GetComponentsInChildren(); // In grid view, one item panel will be active. That's the selected item! UnityEngine.Assertions.Assert.AreEqual(active.Length,1,"Too many items or no item selected!"); // Now we definitely need one to be selected! } // This test case tests if the currently selected item disappears from the view when the buy model confirms the selection [UnityTest] public IEnumerator ModelSelectionRemovesViewItem() { yield return null; var selectedItem = shopView.transform.Find("GridItemsPanel"); shopView.ShopModel.SelectItemByIndex(shopView.ShopModel.GetSelectedItemIndex() + 1); yield return null; var active = selectedItem.GetComponentInChildren(); // In grid view, one item panel will be active. That's the selected item! shopView.ShopModel.ConfirmSelectedItem(); yield return null; UnityEngine.Assertions.Assert.IsTrue(active == null,"Selected item view was not destroyed when bought!"); // Now we definitely need one to be selected! } // Tests if the player inventory doesn't allow to overdraw money. We test on shop inventory even though that one's money's technically irrelevant, but works the same on all [UnityTest] public IEnumerator InventoryDoesNotAllowExceedingTransactions() { yield return null; // So if we pay too much, we expect an exception Assert.Throws(delegate { shopView.ShopModel.inventory.ChangeBalance(-shopView.ShopModel.inventory.Money - 1); // Pay one money more than it's got! }); } // Tests if transaction changes actually change an inventory's balance [UnityTest] public IEnumerator InventoryCorrectlyHandlesBalanceChange() { yield return null; var moneyBefore = shopView.ShopModel.inventory.Money; shopView.ShopModel.inventory.ChangeBalance(-shopView.ShopModel.inventory.Money); // Spend all our money! Assert.AreNotEqual(moneyBefore,shopView.ShopModel.inventory.Money); Assert.Zero(shopView.ShopModel.inventory.Money); shopView.ShopModel.inventory.ChangeBalance(moneyBefore + 1); // For good measure, re-add all money plus a one money bonus Assert.AreEqual(moneyBefore + 1,shopView.ShopModel.inventory.Money); } // Tests if a model throws an error when trying to select an item directly that isn't part of it [UnityTest] public IEnumerator ModelSelectionErrorIfInvalid() { yield return null; Assert.Throws(delegate { shopView.ShopModel.SelectItem(new ItemPotion("some name","invalidIcon",5,2,PotionType.Healing)); }); } // Tests if a model selects an item correctly when it's valid [UnityTest] public IEnumerator ModelSelectionIndexCorrectIfValid() { yield return null; var item = shopView.ShopModel.GetSelectedItem(); shopView.ShopModel.SelectItemByIndex(shopView.ShopModel.GetSelectedItemIndex() + 1); Assert.AreNotSame(item,shopView.ShopModel.GetSelectedItem()); Assert.DoesNotThrow(delegate { shopView.ShopModel.SelectItem(item); }); Assert.AreSame(item,shopView.ShopModel.GetSelectedItem()); } // Tests if a buy model removes an item on confirmation, and then selects another one [UnityTest] public IEnumerator BuyModelConfirmationRemovesItemAndSelectsNew() { yield return null; var item = shopView.ShopModel.GetSelectedItem(); shopView.ShopModel.ConfirmSelectedItem(); Assert.AreNotSame(item,shopView.ShopModel.GetSelectedItem()); Assert.Throws(delegate { shopView.ShopModel.SelectItem(item); }); Assert.AreNotSame(item,shopView.ShopModel.GetSelectedItem()); } // Tests if a buy model removes money from an inventory it buys from [UnityTest] public IEnumerator BuyingItemCostsMoney() { yield return null; var money = sellView.ShopModel.inventory.Money; // In the scene setup, sellView happens to reference the player inventory the shop trades with Assert.DoesNotThrow(delegate { shopView.ShopModel.ConfirmSelectedItem(); }); Assert.Less(sellView.ShopModel.inventory.Money,money); } // Tests if a sell model gains money from selling [UnityTest] public IEnumerator SellingItemGivesMoney() { yield return null; var money = sellView.ShopModel.inventory.Money; // In the scene setup, sellView happens to reference the player inventory the shop trades with Assert.DoesNotThrow(delegate { sellView.ShopModel.ConfirmSelectedItem(); }); Assert.Greater(sellView.ShopModel.inventory.Money,money); } // Tests if selling removes the item from the sell inventory [UnityTest] public IEnumerator SellingItemRemovesFromInventory() { yield return null; var item = sellView.ShopModel.GetSelectedItem(); sellView.ShopModel.ConfirmSelectedItem(); Assert.AreNotSame(item,sellView.ShopModel.GetSelectedItem()); Assert.Throws(delegate { sellView.ShopModel.SelectItem(item); }); Assert.AreNotSame(item,sellView.ShopModel.GetSelectedItem()); } // Tests if upgrading an item does not remove it [UnityTest] public IEnumerator UpgradingKeepsInInventory() { yield return null; var item = upgradeView.ShopModel.GetSelectedItem(); upgradeView.ShopModel.ConfirmSelectedItem(); Assert.AreSame(item,upgradeView.ShopModel.GetSelectedItem()); Assert.DoesNotThrow(delegate { upgradeView.ShopModel.SelectItem(item); }); Assert.AreSame(item,upgradeView.ShopModel.GetSelectedItem()); } // Tests if upgrading costs money [UnityTest] public IEnumerator UpgradingCostsMoney() { yield return null; var money = upgradeView.ShopModel.inventory.Money; // In the scene setup, sellView happens to reference the player inventory the shop trades with Assert.DoesNotThrow(delegate { upgradeView.ShopModel.ConfirmSelectedItem(); }); Assert.Less(upgradeView.ShopModel.inventory.Money,money); } // Tests if upgrading improves an item's stats [UnityTest] public IEnumerator UpgradingImprovesStats() { yield return null; var item = upgradeView.ShopModel.GetSelectedItem(); // In the scene setup, sellView happens to reference the player inventory the shop trades with var stats = item.GetStats(); // Stats right now are turned into a generic string type. If we get a different one post-upgrade, it should've worked Assert.DoesNotThrow(delegate { upgradeView.ShopModel.ConfirmSelectedItem(); }); Assert.AreNotEqual(stats,upgradeView.ShopModel.GetSelectedItem().GetStats()); } } }