Improving Testability Through Design

This course tackles the issues of designing a complex application so that it can be covered with high quality tests.
Course info
Rating
(238)
Level
Advanced
Updated
Aug 11, 2014
Duration
4h 36m
Table of contents
Assessing Reliability of Tests
Developing an Application the Old Way
Guidelines of Redesign for Reliability
Getting the Most Out of Immutable Objects
Improving the Unit Tests
Crossing Responsibility Boundaries
Managing Operations on Database and External Systems
Description
Course info
Rating
(238)
Level
Advanced
Updated
Aug 11, 2014
Duration
4h 36m
Description

A well designed application is not necessarily the one which has a perfect separation of layers, or the one which perfectly implements some predefined design patterns. It is certainly a plus to have these two goals met, but that is not sufficient to make the application really good. We can learn the most about one application by reading the source code of particular methods embedded deep inside of it. A common for loop often reveals more about the design than the whole diagram depicting responsibilities of an application layer in which it is located. The devil is in the details. The best of all intentions in design fails miserably when the low-profile design of small, seemingly unimportant classes is misconceived. In this course, the order of decisions is sorted bottom-up. It is the small class to which we pay attention the most. Only when all things are in place at the microscopic level can we discuss responsibilities of layers, isolation of modules and other high profile topics. The result is a well-built, easily testable and easily maintainable application.

About the author
About the author

Zoran Horvat is Principal consultant at Coding Helmet Consultancy, speaker and author of 100+ articles, and independent trainer on .NET technology stack.

More from the author
Writing Purely Functional Code in C#
Advanced
4h 14m
May 23, 2018
Making Your C# Code More Functional
Intermediate
3h 54m
Jan 24, 2018
Advanced Defensive Programming Techniques
Intermediate
6h 22m
Aug 25, 2017
More courses by Zoran Horvat
Section Introduction Transcripts
Section Introduction Transcripts

Assessing Reliability of Tests
Hi, this is Zoran Horvat and in this module I'm going to talk to you about what makes tests reliable. We can speak about tests in different ways. Whether tests are manual or automated, for example. In this course I'm only going to talk about automated tests. Now about automated tests. We can measure how much of the code is covered with tests. This measure can be made by counting how many lines of code are executed while tests are running. Alternatively we might count how many methods of our classes have been invoked from tests or even how many of the possible execution paths have been taken while executing the tests. Whichever the counting we adopt, we could make up a scale of code coverage, which at the bottom end has projects that do not have automated tests at all and at the top it has projects that are fully covered with tests. There are cases where it is perfectly acceptable not to have any automated tests. For example, proof of concept application might not have any tests with it, or an application which is only going to be used for some demonstration, or application for which we know that nobody's ever going to ask for any maintenance or upgrade, but if there is a realistic chance that we might need to make changes to the code, having no automated tests might be a problem. By making one change in the code we might create inadvertent changes in some other parts of the application that might cause defects to appear in those other parts of the application.

Developing an Application the Old Way
Hi, this is Zoran Horvat again and in this module I'm going to write a simple domain model and the accompanying demo application. What I plan to do is to actually write a very fragile and poorly designed application, but it is not my desire to show you how bad a coder I could be. No. I will actually write the code in a way that I see widely adopted in practice. The goal of this exercise is to show how some of the usual techniques in design and coding have adverse effects on code quality. There will be many annoying defects that are hard to foresee and cover with tests, but easy to discover when application gets into production. There will be no time in this module to fix the issues so do not expect quick solutions to the issues that I'm going to cause. By the end of the module, application will be in a very sad condition, but it will provide a great starting point for making the things better. Now I will stop talking here and get back to the Visual Studio to do some real coding. Now that I have this idea to leave the length and width properties in this class, I need to cover them with tests, but note that I am not covering these properties with tests only because they exist. There is a deeper reason. The reason is that these properties contain logic. As soon as you add an if statement into a property, you must test it. Because this if statement makes a property do one thing or the other. To be sure that the property setter is doing the right thing, we must to set different values to it and see whether it makes the right decision in each of the cases. If I only had a property getter and setter, something like this, I would not need to cover that with tests. There is no logic in this property, but as I said, length and width properties do contain logic and that is the reason why I am covering them with tests.

Getting the Most Out of Immutable Objects
Hi, this is Zoran Horvat again. Welcome to the module in which I am going to talk about how to use immutable objects. If you have any experience with Haskel or F# or any other functional programming language, then you have probably accumulated a lifetime dose of knowledge about immutable objects. But even if you do not have experience in functional programming, the root piece of theory is surprisingly simple. Consider date and time value for example. This value has a non-trivial structure. It consists of year, month and day numbers. Its time part consists of hours, minutes, and seconds and optionally fractions of seconds such as milliseconds, but all these values can easily be represented using one relatively large number. For example, I could represent date/time value as total number of milliseconds that irrevocably passed since my birthday. That would take only 41 bits of information. Too much for a 4-bit integer, but fits easily into a 64-integer number. It should not surprise anyone that complete date and time take together are typically considered to be a single value regardless of an apparently nontrivial structure. We can compare date/time values, test them for equality, calculate their difference in days or seconds, etc. Generally, any object can be treated as a value. Even large and complex objects such as domain objects, binary trees, and similar can be treated as values. One very important characteristic of values is that they are constant during their lifetime. If a value changes, then it becomes some other value.

Improving the Unit Tests
Hello again. This is Zoran Horvat. Welcome to the module in which I will be talking about several distinct kinds of automated tests. I will begin with the unit tests, which are the simplest of them all. In unit tests, test method creates an object and invokes an operation on it. After that, the test method verifies the expectation. This verification can be a form of testing the returned value or testing the state of the object. It would also be possible to test state of some other object which is referenced by the object on the test. But then that would not be the unit test any more. It would become an integration test. But there is a lot of room between these two extremes. I will first introduce the mocked objects to the test. This will take me to a higher ground where I will be able to control the level of isolation between the class and its collaborators. At the lower end of the specter, the class and the test will be fully isolated. This means that all dependencies that take part in a test will be mocked. This change will impact the test in two ways. The first impact is when a class on the test remains the only entity to be tested. Mocks are there to just fill the gap and replace the concrete dependency. This is the measure that saves the test from failing due to an error in the dependency, but then I will make another step towards the outer world and try to see what signals the class and the test sends to its dependencies. I will focus on quantifying this interaction. At that time I will be in the land of interaction tests. It will be very exciting to write tests in that way, you will see. But there will be the case here and there when this approach comes short. Instead of sticking to the idea of mocking at all costs, I will make a turn and give up the mocks entirely in those cases. Actually, you will see that it is very simple to recognize when mocking starts causing more troubles than it solves.

Crossing Responsibility Boundaries
Hi. This is Zoran Horvat and this is the module in which I will discuss class responsibilities. You will see what it means that some class is responsible for something and probably even more important, what it means that the class is not responsible for something else. Knowing the difference is knowing how to produce a consistent design. Design which is not going to urge for changes as soon as it is deployed. Now what is the responsibility of a class? Suppose that we have a class and that this class needs to perform some operation. The operation is the responsibility of this class if it naturally belongs to the class. If the operation works only on data contained in the object, if it is complete and will not require modification later, then we can say that it is the responsibility of this class to expose and implement this particular operation, but what if we find that it is the other way around? What if we find that the operation requires data or features that are already part of some other class? Then we just move it out to that other class where it fits more naturally. We create a dependency relationship between current class and the new one. Now the class depends on an external class in terms that it cannot complete its own operation unless it makes a call to the dependency. This call is intended to make the operation complete. Without it, the current class would not be able to complete the operation elegantly. Its implementation would include the code which clearly doesn't fit there. It is much better to pull out parts of the operation and to enclose them into classes where they feel like home. The code becomes simpler. It is then easier to read and to understand what it does. Also, one very important motive. The code is then easier to maintain because two classes are changed or extended separately. You will see examples how it goes when we begin maintaining the class by the end of this course.