In this tutorial, I will talk about the coding practices that I found to be the most beneficial in my experience.
The most important practice is Automated Testing. More specifically, the practice of making sure that simple tests are written that allow you to verify that you don’t break anything when you modify your code.
In this part, I will talk about this.
Note: In this article, I give you advice based on my 9+ years of experience working with applications and sharing my knowledge. Although I have worked with many kinds of applications, there are probably kinds that I did not work with. Software development is more of an art than a science. Use the advice I give you if it makes sense in your case.
Coding Practice: Having tests that pin program behavior
Make sure that you have tests that pin the behavior of your programs. Here are the properties of the tests I am talking about:
1. Require low maintenance
A good test will not need to be modified often. A very good test will almost never be modified once it is written.
Before talking about writing low maintenance tests, let’s talk about visibility layers.
Figure 1: Visibility layers in some program
Figure 1 illustrates the visibility layers of some program. I use the word “layer” here in a very general sense. I am not trying to talk about a specific architecture of a program. But in general, all programs have some sort of visibility layers. That is, there are parts of the program that are more visible to the outside world, and others that are internal and less visible from the outside.
The green squares in Figure 1 represent the outer visibility layer of the program – i.e. the most visible parts of the program. This may include the user interface, the database, the public API of the program (e.g. when the program is a web service), etc. The blue squares represent the code at a more internal layer. The white squares represent code at an even more internal level.
For example, the blue squares might contain code that is immediately below the UI, e.g. View Models in an MVVM application, or code that usually sits underneath the UI. These squares could also contain data access code that is just above the database itself, e.g. an ORM.
When writing tests, we can choose to use the green blocks as the points of interaction between the tests, and the system under test.
Figure 2: Using the outer visibility layer for testing
For example, we can write a test that interacts directly with the user interface (UI tests), and then checks that certain data has been put inside the database. As a more concrete example, we can use a UI testing library such as the Microsoft’s Coded UI component to interact directly with the buttons and other input fields of some UI.
Also, in the assertion phase of our test, we can directly access data in a Microsoft SQL Server table to see that data was inserted as expected by the test. Alternatively, the assertion phase can also be done against the UI. For example, we can navigate to some reporting section of the UI and verify that the data that was given as input to the UI, is now part of some report that is visible in the UI.
We can also choose a more internal visibility layer to do that. See Figure 3 with an area surrounded in red.
Figure 3: Using the second most visible layer for testing
For example, instead of writing a test that interacts with the user interface, we can write a test that interacts with the View Models in some MVVM application. Also, instead of checking data in the database server at the assertion phase, we can use an in-memory database in the test. Or, if we are using the repository pattern, we can use a fake repository object for testing.
We can also test internal code. We can test a single unit of behavior, or a group of units.
Figure 4: Testing a group of internal units of behavior
An example of such a test is a test that tests a few classes together. Figure 4 depicts this.
Figure 5: Testing a single unit of internal behavior
Figure 5 depicts testing a single unit of internal behavior, e.g., testing a single class.
The boundary of the tests can be anything really.
Figure 6: A test that spans different visibility layers
For example, we can invoke the program using the View Models of some MVVM application, and then assert against the internal state of some internal class in the program. I am not saying that you should do this, I am just exploring the possibilities.
Different tests have different advantages and disadvantages.
Although many kinds of tests can be important, here I only want to talk about the most important ones. That is, I want to talk about the ones that I consider doing them as the number one best practice.
For tests to require low maintenance, they should be written using points of interaction that are less likely to change. The following can change easily:
- Internal code: The internal blocks of our programs are likely to change. One day we may decide to implement a feature in a certain way. The next day we decide to do it another way for whatever reason. We might delete existing functions/classes and add new ones. Or we might change the signatures of these functions/classes. Such changes immediately break tests that are done against such functions/classes.
- Volatile outer layer code: The outer visibility layers of a program are closer to the user requirements. For example, the UI is what the user sees. The user cares more about the user interface and the functionality it provides than the internal implementation details behind it.
The public API of a web service is what the consumer of the service sees. The consumer of the service cares about this API and doesn’t really care about the internal implementation details.
Although such outer visibility layers are closer to user requirements, some kinds of components in such layers are still volatile and can change. The user interface is one example. Although the core features of the UI are relatively stable, the UI itself is volatile.
To explain this further, consider a program that allows users to translate some documents in a folder. The user is expected to give the following inputs to the program:
i. The input folder path
ii. Authentication settings for the translation server
iii. Output document format
The core UI feature here is to input these three pieces of data. However, you can implement this via different kinds of UI features, e.g.:
i. You can have all these fields in a single form, or you can have a wizard where each page of the wizard asks for only one of these inputs.
ii. You can use a single text box and a pick button that opens a dialog that allows you to pick the folder from the folder tree of the file system like Figure 7, or you can have the file system folder tree available directly on the form like Figure 8.
Figure 7: Picking the input folder by clicking a pick button first
Figure 8: Picking the input folder directly
iii. You can use a drop-down list (e.g. via a ComboBox) to let the users select the output document format or you can use radio buttons.
As you can see, although the core UI functionality is to input three pieces of information, the UI itself can be anything, and the exact way we implement it can easily change.
There are other components that are volatile. For example, if your program communicates with an external web service to calculate exchange rates, the web service might be down, and you cannot control the data returned by such web service.
So, to make tests require low maintenance, we should write them against non-volatile components in the most possible outer visibility layer. For programs with a UI, a subcutaneous test might be what you need. For web services, the service API is usually the best places to use for tests.
2. Be deterministic
A failing test should always mean there is a problem. The test shouldn’t fail because an external web service is down. It shouldn’t fail because the currency exchange rates (which are returned by the external service) have changed. Fakes can usually be used to fix such issues. Note that some dependencies might be acceptable.
For example, a local database (a special testing database controlled by the tests) might be acceptable. The price to pay in terms of indeterminism (that the local database might be down) in this case may (or may not) be relatively low compared to the value of testing against a real database.
3. Be close to user requirements
This is very much related to Pratice #1. Because user requirements change slower than implementation details and because they are mostly incremental in nature, having tests at the level of user requirements makes them less likely to change.
4. Run relatively quickly
The tests I am talking about here, run quickly.
Enough to be able to run the possibly hundreds or even more tests in a few minutes. Although it is best for a test to take less than a few milliseconds, it might be fine if a test takes a second or a few seconds. There is a lot of advice out there that tests should run even quicker than this. My problem with super-fast tests is that they usually test small internal blocks of code that are implementation details which would then make the tests require high maintenance.
If you can have super-fast end-to-end tests, that would be great. If not, making the tests low-maintenance is more desirable. Of course, there might be cases were end-to-end tests are super-slow. In that case, using a lower visibility layer might be a good option. The important thing is to realize the cost and value of testing at each layer and then deciding on the best option.
One advantage of super-fast tests is that you can run them frequently, even as you are typing code. Although this has value, the value of having low-maintenance end-to-end tests is higher in many cases. I usually run tests a few times every day, and that is enough for me.
Another thing that might be good to do when creating a test suite is to add an additional layer for testing. That is, the tests themselves will not invoke the system under test directly, instead they will invoke it indirectly.
For example, if you are testing a program which is a translation web service, you can have a method in your test class called InvokeSUT that the tests call. InvokeSUT would in turn invoke the translation web service that is under test. This way, if there are minor changes to the public interface of the web service under test, only the InvokeSUT method will need to be changed instead of all the test methods.
This article is about the coding practices that I found to be the most beneficial during my work in software development. In this part, Part 1, I talk about the most important practice: having low-maintenance relatively-quick-to-run end-to-end tests that pin the program behavior.
I will talk about the next practices in the upcoming articles.
This article was technically reviewed by Damir Arh.
This article has been editorially reviewed by Suprotim Agarwal.
C# and .NET have been around for a very long time, but their constant growth means there’s always more to learn.
We at DotNetCurry are very excited to announce The Absolutely Awesome Book on C# and .NET. This is a 500 pages concise technical eBook available in PDF, ePub (iPad), and Mobi (Kindle).
Organized around concepts, this Book aims to provide a concise, yet solid foundation in C# and .NET, covering C# 6.0, C# 7.0 and .NET Core, with chapters on the latest .NET Core 3.0, .NET Standard and C# 8.0 (final release) too. Use these concepts to deepen your existing knowledge of C# and .NET, to have a solid grasp of the latest in C# and .NET OR to crack your next .NET Interview.
Click here to Explore the Table of Contents or Download Sample Chapters!