📜 ⬆️ ⬇️

Assert DSL on the example .Net

No one denies the usefulness of tests in any complex system. Without tests, you can quickly go into chaos and spend most of your time in the debugger, searching for and catching indirect effects from changes in one or another part of the application. Tests are important, necessary, and so on.

According to science, tests are system documentation. Competently written tests make it clear how the system works, how it behaves, and all this should be read as a ready-made specification of the behavior of the system. Those. ideally, a coherent and understandable text should be obtained. This is an ideal, to which test methods are gradually approaching, starting from unit testing and most clearly manifested in behavioral / acceptance testing, when the tests themselves are already written in the business language (recall the Fitnesse at this point).

When writing tests, you should not skimp on lines of code and classes, it is only important to structure them correctly. I believe that it may be quite normal situation when you have a test class consists of only one test method - do not be ashamed of this, it is much better than classes on 20 screens. HD screens.
')
In general, everything should be directed to the maximum clarity and clarity of tests, so that all interrelations are clearly visible. To be able to restore the logic of the program for only one test. Not only Assert DSL (Domain Specific Language), but also file naming, the Arrange Act Assert approach will go into the matter of readability. All this is not new approaches as it turns out, but not widely known yet, judging by what I see in the projects around me. Yes, and I myself came across new topics by chance, studying the source codes of StructureMap.

In order not to torment, I will immediately tell you what basic steps are proposed to improve the tests:

I think that for the majority of many of the items listed are not news, and almost all of them are used in real development.



In a broad sense, this fits into the paradigm of the Arrange Act Assert , which suggests that it is necessary to clearly distinguish the preparation for the test, the action, the test. In this case, it turns out that each test class will describe a specific preparation for the test. In SetUp or in FixtureSetUp, the Act will ideally be specified and the tests will already check the result - Assert.

It is best to show it with an example.

Suppose we have a class Pirate, which is the implementation of the actions and capabilities of the pirate in the game. A pirate can move around the field, pick up and leave gold, fight, swim, kill and die. A lot of things he could and it would be wrong to push all the tests into one file and demarcate the tests by regions. It is much better to make several test classes, for example:

Hmm, the pirate is not so branched and more methods to devote them to a separate class. But then we have a playing field, in which there are more responsible methods. Suppose a class Field which is responsible for creating the playing field and general movement control. His tests will be:

Then the tests inside the class name for example:

Then when viewing the tests and results, you can read them as When [game] Create Field [ it] Should Generate Sea On [ field's] Border is almost pure English. With pirates, you just need to write a little more in the method name, i.e.

In the case when we can call the test class from the word When , this is a pure example on the Arrange Act Assert. For example:

Example:
[TestFixture] public class WhenCreateField { private Field field; private TestEmptyRules rule; [TestFixtureSetUp] public void ClassInit() { // Arrange, Act rule = new TestEmptyRules(); field = rule.Field; } [Test] public void MaxSizeShouldBeDefined() { //Assert Position.MaxColumn.ShouldBeEqual(rule.Size); Position.MaxRow.ShouldBeEqual(rule.Size); } [Test] public void FieldShouldBeCreated() { //Assert field.ShouldBeNotNull(); } [Test] public void ItShouldBeGrassByDefault() { //Assert field.GetPlayableArea() .ShouldContain() .OnlyCellsOf(CellType.Grass); } … } 

All the preparation is done in the initialization of the test class, and then only the checks go.

One test - one test. This can be seen in the examples above. Immediately it is clear what is being tested and what should be the result. Often there is a great temptation to add Assert to an already existing test - this is called “add a bunny” here, and so “bunnies” can then get a bit worse because they will embarrass minds and steal extra time when raising tests after refactoring.

Further, the structuredness of the tests is very important. Compare:
 [Test] public void HeMayKillFoes() { var airplaneCell = new AirplaneCell(4, 5); var player = Black.Pirate; var foe = Red.Pirate; airplaneCell.PirateComing(foe); airplaneCell.PirateComing(player); airplaneCell.Pirates.ShouldContain().Exact(player); } [Test] public void HeMayKillFoes() { //Arrange var airplaneCell = new AirplaneCell(4, 5); var player = Black.Pirate; var foe = Red.Pirate; airplaneCell.PirateComing(foe); //Act airplaneCell.PirateComing(player); //Assert airplaneCell.Pirates.ShouldContain().Exact(player); } 

Despite the simplicity of the tests, and they should be simple, the first option, in my opinion, will take more time to realize where the preparations are underway, where the key method will be checked, and where the verification itself is. In the second variant, the look easily determines the boundaries of the components and the mind is quicker aware of key points. I guarantee that you yourself will then be easier to examine your tests and recall what exactly is being tested.

Another example:
  [Test] public void AtSecondTimeHeCanNotTransferToShip() { //Arrange var airplaneCell = new AirplaneCell(4, 5); var pirate = Black.Pirate; airplaneCell.PirateComing(pirate); //Act airplaneCell.Transfer(); airplaneCell.PirateComing(pirate); airplaneCell.Transfer(); //Assert pirate.State.ShouldBeEqual(PlayerState.Free); } 

Key points are highlighted and the case for which the test is created is immediately clear. At corridor testing of this approach, it turned out that the Act is the most controversial point in writing tests. Different people often see differently what needs to be included in the test setup, and what is included in the action being tested. The same moment is mentioned in all articles devoted to AAA, the same answer is given, to which we arrived with our colleagues: distinguish between what you consider necessary and how you agree. Yes, thank you cap! There are no strict rules.

Now the key point, which probably already noticed the most attentive and writing tests comrades. In tests there is no explicitly Assert construction.

Honestly, I like this approach much more, because the first impulse is to write the property that needs to be tested . Then you already understand that you need to enter the desired Assert, which I used to hang abbreviations for InteliSense. For example, for Assert.AreEqual / Assert.That ($ actual $, Is.EqualTo ($ expected $)) was aae , i.e. I typed this combination, pressed Tab and already have a pattern in the code. But this is inconvenient, it was necessary to configure ReSharper, remember that assert goes first.

It is much more convenient to use the potential of the language in terms of writing extension methods and using it. Thus, you will always have a hint for all developers, regardless of whether they use ReSharper or CodeAssist, or some other system.



In the illustration above you can see that the code is written on an intuitive, semantic level. The first impulse is to write the value for testing, then we think how and with what to compare it, and the last step is to record the desired value. Please note that IntelliSense suggests the type of expected value to check.



This illustration shows the standard approach to writing validation for nUnit. We must remember that the service code Assert.That comes first. IntelliSense does not always help when writing a value for verification. The illustration shows the real work without scrolling to any item in the drop-down list. After writing the value to check, again, you need to “remember” the service word, write the type of check and when entering the expected IntelliSense value is powerless.

Next, compare visually two approaches:
 [Test] public void FieldShouldBeCreated() { //Assert field.ShouldBeNotNull(); } [Test] public void FieldShouldBeCreated() { //Assert Assert.IsNotNull(field); } 

And another example:
 [Test] public void MaxSizeShouldBeDefined() { // DSL Position.MaxColumn.ShouldBeEqual(rule.Size); Position.MaxRow.ShouldBeEqual(rule.Size); // nUnit Assert.That(Position.MaxColumn, Is.EqualTo(rule.Size)); Assert.That(Position.MaxRow, Is.EqualTo(rule.Size)); // MSTest Assert.AreEqual(Position.MaxColumn, rule.Size); Assert.That(Position. MaxRow, rule.Size); } 

It seems to me that the first options, where extension methods are used, are more understandable and easy to read. The main advantage is that you can clearly see what is and what is expected . Although nUnit is good in this respect, which somewhat levels the difference in approaches, compared to MSTest, this is heaven and earth. In general, MSTest does not understand for beginners where the expected result is, and where it is received.

Another advantage of writing extension methods for checking tests can be their logical structure, consonant with the domain model. It is best to demonstrate this again with an example:
 [Test] public void ItShouldBeGrassByDefault() { //Assert field.GetPlayableArea() .ShouldContain() .OnlyCellsOf(CellType.Grass); } [Test] public void ItShouldBeGrassByDefault() { //Assert var playableArea = field.GetPlayableArea(); Assert.IsTrue(playableArea.All(cells => cells.CellType == CellType.Grass)); //or another case Assert.IsTrue(field.GetPlayableArea() .All(cells => cells.CellType == CellType.Grass)); } 

Which way is clearer?

I think it’s easy to develop your own test DSL for a specific use. But as an example (not yet polished use):
 public static CollectionAssertCases ShouldContain(this IEnumerable enumerable) { return new CollectionAssertCases(enumerable); } public class CollectionAssertCases { private readonly IEnumerable enumerable; public List AsList { get { return new List(enumerable); } } public CollectionAssertCases(IEnumerable enumerable) { this.enumerable = enumerable; } public void Elements(params T[] elements) { Assert.That(enumerable, Is.EquivalentTo(elements)); } } public static void OnlyCellsOf(this CollectionAssertCases collection, CellType cellType) { Assert.IsTrue(collection.AsList.All(c => c.CellType == cellType)); } 

Besides the fact that DSL for testing becomes more understandable and concise, it allows you to get rid of technically unimportant details. Reduce duplication of code and increase the speed of writing tests.

Another useful application can be found in technical code testing. For example, I do not want any class property to suddenly become writable, and I can write a test for it easily.
 [Test] public void PiratesStateCanNotBeSetDirectly() { //Arrange var pirate = Black.Pirate; //Assert pirate.Property("State").ShouldBeReadonly(); } 

The test is not cluttered with unnecessary methods and actions when working with reflection, but gives control over changing the API, if you work in a large or inexperienced team where it is not always possible to agree on one or another use of properties. It works the same way as “foolproof”. Agree that the test will not cause particularly vivid emotions, even among colleagues, ossified in their rejection, of reflection.

Again, following the results of corridor testing on colleagues, it was discovered that the power of habit is a great thing - to immediately read the tests and write them is somewhat unusual. The eyes are looking for a static class Assert and do not find what causes a slight bewilderment, while it turned out that the word Assert in the comments for some reason does not rush into the eyes. I think this will pass quickly, since the flexibility of the mind must be present to the developers though.

The advantages to the described approach include:

By cons I would take:

Source: https://habr.com/ru/post/124254/


All Articles