-
Notifications
You must be signed in to change notification settings - Fork 289
Integration test tutorial: Your First Integration Test #649
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
d60a46c
4d5d39e
fe0e9b5
b44edf5
7ef32b0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,170 @@ | ||||||||||||||
| # Your First Integration Test | ||||||||||||||
|
|
||||||||||||||
| In this guide you will learn about integration testing and how to create an integration test. | ||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should also probably say "this guide assumes you have some familiarity with c#, go read the bikehorn guide if you havent already" |
||||||||||||||
|
|
||||||||||||||
| ### What is integration testing? | ||||||||||||||
|
|
||||||||||||||
| **Integration testing** is a useful tool to ensure that changes to one part of the game doesn't unexpectedly cause another part of the game to change too. | ||||||||||||||
| It can catch unintended behavior, bugs and even rare game-crashing errors when used properly! | ||||||||||||||
| This is achieved through **integration tests**, which basically run short simulations of the game and makes sure ingame values match what the test expects. | ||||||||||||||
|
Comment on lines
+7
to
+9
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. term
Suggested change
|
||||||||||||||
|
|
||||||||||||||
| An example would be changing a Cargo order to cost less. | ||||||||||||||
| If you have an integration test that compares order costs to sell values, you'll be able to automatically catch if this change would result in an infinite money loop! | ||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||
|
|
||||||||||||||
| Integration tests are ran on all pull requests submitted to the SS14 repository and all tests must pass for a PR to be mergeable. | ||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||
| You can also run tests locally in your IDE (useful if you fail a specific test when submitting a PR). | ||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. maybe add a link to src/en/general-development/tips/debugging-tools.md after #629 is merged to clarify how this is done locally |
||||||||||||||
|
|
||||||||||||||
| ### The structure of a test | ||||||||||||||
|
|
||||||||||||||
| Tests generally follow this flow: | ||||||||||||||
|
|
||||||||||||||
| - Select which base test class the test should use. | ||||||||||||||
| - Define test-specific prototypes & settings. | ||||||||||||||
| - Spawn entities and retrieve components/systems to test. | ||||||||||||||
| - Assert default values (i.e. "are the starting values what I expect?"). | ||||||||||||||
| - Do the test scenario. | ||||||||||||||
| - Assert that values have changed (i.e. "did the test result in what I expected?"). | ||||||||||||||
|
|
||||||||||||||
| We will go through this flow in the tutorial below: | ||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||
|
|
||||||||||||||
| ## Making your first test | ||||||||||||||
|
|
||||||||||||||
| In this tutorial we are going to make a test to check that hugging works. | ||||||||||||||
| Hugging is done via `InteractionPopupSystem` and `InteractionPopupComponent`, and when a hug is performed `InteractionPopupComponent.LastInteractTime` should get updated to a new value. | ||||||||||||||
|
|
||||||||||||||
| We decide our test will try to simulate a hug and then verify that it happened by checking if `LastInteractTime` updated. | ||||||||||||||
|
|
||||||||||||||
| ### Setup | ||||||||||||||
|
|
||||||||||||||
| Integration tests are created in a relevant area folder in `Content.IntegrationTests/Tests`, so we create a new folder `InteractionPopup` and a new C# script `InteractionPopupTest`. | ||||||||||||||
|
|
||||||||||||||
| Our first decision will be to choose which base test class to use. | ||||||||||||||
| These are used to handle boilerplate (e.g. setting up and disposing of finished tests) and enable specific functionalities (such as spawning a default player mob or a walkable grid). | ||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. could possibly define 'boilerplate' here in case someone hasnt encountered the term |
||||||||||||||
| Some choices include `GameTest`, `InteractionTest` and `MovementTest`. | ||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. are test classes found in a specific directory? where should someone be looking when they want to figure out what base class to use? |
||||||||||||||
|
|
||||||||||||||
| For our test, we will use `InteractionTest` as our base class. | ||||||||||||||
| It spawns a simple player mob with a single hand and has a lot of helper functions related to interactions that we can make use of later: | ||||||||||||||
| ``` | ||||||||||||||
| using Content.IntegrationTests.Tests.Interaction; | ||||||||||||||
|
|
||||||||||||||
| namespace Content.IntegrationTests.Tests.InteractionPopup; | ||||||||||||||
|
|
||||||||||||||
| public sealed class InteractionPopupTest : InteractionTest | ||||||||||||||
| { | ||||||||||||||
|
|
||||||||||||||
| } | ||||||||||||||
| ``` | ||||||||||||||
| We will also create the method inside of which our test is run, `HugTest()`. The method requires two properties: | ||||||||||||||
| - A `[Test]` attribute, to mark the method as a test. | ||||||||||||||
| - The `async` keyword, since the test simulation will run alongside other tests, and some behaviors (such as spawning) will take time to run. | ||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should definitely include a note here on test memory usage, and maybe point to some tests where |
||||||||||||||
| ``` | ||||||||||||||
| using Content.IntegrationTests.Tests.Interaction; | ||||||||||||||
|
|
||||||||||||||
| namespace Content.IntegrationTests.Tests.InteractionPopup; | ||||||||||||||
|
|
||||||||||||||
| public sealed class InteractionPopupTest : InteractionTest | ||||||||||||||
| { | ||||||||||||||
|
|
||||||||||||||
| [Test] | ||||||||||||||
| public async Task HugTest() | ||||||||||||||
| { | ||||||||||||||
|
|
||||||||||||||
| } | ||||||||||||||
| } | ||||||||||||||
| ``` | ||||||||||||||
| With this, the test should now be visible in the Tests tab of your IDE! | ||||||||||||||
| Exactly where the Tests tab is located depends on the IDE you use, but if once found you should be able to see `InteractionPopupTest` among the other test folders. | ||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||||||||||||||
| You can even run the test if you want, though since the test is empty it will just return a Success. | ||||||||||||||
|
|
||||||||||||||
| ### Spawning an entity | ||||||||||||||
| Since `InteractionTest` handles spawning the player mob automatically, our first actual step in creating the test will be to spawn in the mob we will hug. We have two options here: | ||||||||||||||
|
|
||||||||||||||
| - Rely on an existing mob prototype with `InteractionPopupComponent`. | ||||||||||||||
| - Create a dummy prototype inside the test class to only use for this test. | ||||||||||||||
|
|
||||||||||||||
| We will choose the first one since the `MobHuman` prototype is a base mob that we expect will always be huggable, and using it additionally makes the test keep an eye on if that prototype ever accidentally gets changed. | ||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. could you also provide example syntax of how to write a new dummy prototype in the class? & note when it might be helpful to define a dummy- eg, if you are testing how hugging works when someone has a specific component, if defining an entity in the class takes more time... etc |
||||||||||||||
|
|
||||||||||||||
| `InteractionTest` has a built-in spawning method `SpawnTarget`, which spawns an entity one tile next to the player entity and sets it as the target for any future interactions of the player entity. | ||||||||||||||
|
|
||||||||||||||
| ``` | ||||||||||||||
| [Test] | ||||||||||||||
| public async Task HugTest() | ||||||||||||||
| { | ||||||||||||||
| var urist = await SpawnTarget("MobHuman"); | ||||||||||||||
| } | ||||||||||||||
| ``` | ||||||||||||||
|
|
||||||||||||||
| ### Checking for components | ||||||||||||||
|
|
||||||||||||||
| `InteractionTest` has a helper method to get the server component: `Comp<T>(NetEntity? target)`. This also checks that the component exists on the entity, and fails the test if it doesn't. | ||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. maybe an image or inline code of where in the test this helper method was located, how you found it, etc |
||||||||||||||
|
|
||||||||||||||
| ``` | ||||||||||||||
| [Test] | ||||||||||||||
| public async Task HugTest() | ||||||||||||||
| { | ||||||||||||||
| var urist = await SpawnTarget("MobHuman"); | ||||||||||||||
| var interactionPopupComp = Comp<InteractionPopupComponent>(urist); | ||||||||||||||
| } | ||||||||||||||
| ``` | ||||||||||||||
|
|
||||||||||||||
| ### Asserts | ||||||||||||||
|
|
||||||||||||||
| What we want to do is *assert* that the property has the value we expect it to have, and if it doesn't the test should fail. | ||||||||||||||
| The `Assert` class enables this, with the method [`Assert.That`](https://docs.nunit.org/articles/nunit/writing-tests/assertions/assertion-models/constraint.html) being the preferred method of evaluating property values. | ||||||||||||||
|
|
||||||||||||||
| `InteractionPopupComponent` has the property `LastInteractTime`, and while we can *assume* that it will always start at the default value, core to testing is never assuming if you can test it. We can check this with `Is.Default`. | ||||||||||||||
|
|
||||||||||||||
| ``` | ||||||||||||||
| [Test] | ||||||||||||||
| public async Task HugTest() | ||||||||||||||
| { | ||||||||||||||
| var urist = await SpawnTarget("MobHuman"); | ||||||||||||||
| var interactionPopupComp = Comp<InteractionPopupComponent>(urist); | ||||||||||||||
|
|
||||||||||||||
| Assert.That(interactionPopupComp.LastInteractTime, Is.Default); | ||||||||||||||
| } | ||||||||||||||
| ``` | ||||||||||||||
|
|
||||||||||||||
| ### Simulation & Checking | ||||||||||||||
|
|
||||||||||||||
| `InteractionTest` has many helper methods used for simulating interactions. | ||||||||||||||
| With our testcase being simply clicking on the huggable entity, we can use of the basic `await Interact();` method to simulate hugging. Since we spawned the `MobHuman` with `SpawnTarget` earlier, all we have to do is run the method! | ||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||
|
|
||||||||||||||
| Since the player entity spawns with one free hand, we should expect a basic interaction to result in the `InteractionPopupSystem.InteractHandEvent` event subscription triggering, and therefore `LastInteractTime` should be updated to the current time. We assert that the previous `LastInteractTime` should not be equal to the new `LastInteractTime`. | ||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i was initially going to note that you might be able to talk here about how you could go above and beyond and check that they actually spawn with a hand, but then i realised that |
||||||||||||||
|
|
||||||||||||||
| ``` | ||||||||||||||
| [Test] | ||||||||||||||
| public async Task HugTest() | ||||||||||||||
| { | ||||||||||||||
| var urist = await SpawnTarget("MobHuman"); | ||||||||||||||
| var interactionPopupComp = Comp<InteractionPopupComponent>(urist); | ||||||||||||||
|
|
||||||||||||||
| Assert.That(interactionPopupComp.LastInteractTime, Is.Default); | ||||||||||||||
|
|
||||||||||||||
| var previousInteractTime = interactionPopupComp.LastInteractTime; | ||||||||||||||
|
|
||||||||||||||
| await Interact(); // Perform the hug! | ||||||||||||||
|
|
||||||||||||||
| Assert.That(interactionPopupComp.LastInteractTime, !Is.EqualTo(previousInteractTime)); | ||||||||||||||
| } | ||||||||||||||
| ``` | ||||||||||||||
|
|
||||||||||||||
| If you run the `HugTest()` test now, it should pass! | ||||||||||||||
| If any future changes accidentally makes another empty-handed action override hugging, this test will now be able to catch that. You made your first test! | ||||||||||||||
|
|
||||||||||||||
| This tutorial only brushes the surface of how tests can be made. | ||||||||||||||
| The test can expand to cover trying to hug with an item in the player's hand, hugging all different player species, checking that hugs don't come out faster than the cooldown and much more. | ||||||||||||||
|
|
||||||||||||||
| ## Extra Credit: How Do Tests Work Under The Hood? | ||||||||||||||
|
|
||||||||||||||
| There is a lot going into the setup of integration testing that the test base classes do automatically when initialized. | ||||||||||||||
| It can be good to understand this process since a lot can be modified and extended, and there are several helper methods that can save time and make your tests much better. | ||||||||||||||
|
|
||||||||||||||
| `PoolManager` is a static core class that manages server-client simulation relationships, and is used for tests, benchmarks and map rendering. | ||||||||||||||
| For tests specifically it allows for client-servers to be reused for multiple tests and for tests to be run in parallel, instead of constantly starting and shutting down such systems. | ||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. add that this makes tests run fast (good thing (because they usually take a long time)) |
||||||||||||||
|
|
||||||||||||||
| It's unlikely you will access `PoolManager` yourself, but a key property that all integration tests make use of is the `TestPair` class. | ||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. maybe an example of an integration test which uses |
||||||||||||||
| `TestPair` gives access to the Client and Server instances and therefore the ability to set CVars, resolve manager/system dependencies and map management. | ||||||||||||||
|
Comment on lines
+166
to
+167
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 'gives access' could be a little clearer. 'gives the test access'? |
||||||||||||||
| The test base classes all make use of this to create helper methods and properties. | ||||||||||||||
|
|
||||||||||||||
| It is strongly recommended you check out `GameTest.Entities.cs`, `GameTest.Pair.cs` `InteractionTest.Helpers.cs`, `Pair/TestPair.Helpers.cs` and `Pool/TestPair.Helpers.cs` to see what helper methods are available! | ||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. external resources for more reading about test environments outside of robust maybe? |
||||||||||||||

There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe a small note clarifying the differences between integration and unit testing, and if theres ever a situation where it is more appropriate to create a unit test?
could also use
[...]about SS14 integration testing and[...]to note that integration tests are a universal concept in software development. (i did not know this until recently as ss14 was my introduction to software dev :p )