Assert Yourself
The .NET framework provides a Debug class to help debug your code. One of the most powerful methods in this class is Assert. This method is static and returns void in C#, and is a shared sub in VB.NET.
An Assert statement both documents what you believe is happening in your code at any given moment and forces an error and notification if that assumption is incorrect.
.NET provides both a Debug.Assert statement and a Trace.Assert method. Debug methods are removed from your code in release mode, but Trace methods are not (or can be toggled on and off from configuration files (see Chapter 22).
You can see a good (if contrived) use of the Assert statement by adding a third menu choice, "menuTestDivsion," whose event handler is shown here:
private void menuTestDivision_Click( object sender, System.EventArgs e) { Employee joe = new Employee("Joe",47,5); Double pctgOfLife = joe.Age / joe.YearsOfService; lblDisplay.Text = joe.Name + " has worked here for " + pctgOfLife + "% of his life!"; }
When you run this code and choose the Test Division menu choice, you learn that Joe has worked at the company for 9 percent of his life. What happens, however, if the value passed in as years of service is 0. You will then divide 47 by 0, which is illegal, and your program will crash.
The right approach is to test for zero years of service, and if it is found, to skip the division. Assume for a moment, however, that you have other code that prohibits creating a user with zero years of service. Is it now safe to proceed with the division? In theory, yes. But to quote Pat Johnson one more time, "In theory, theory and practice are the same, but in practice they never are."
To ensure that your assumptions are correct, you may want to assert that the years of service cannot be zero. Do so by using the Debug.Assert method with up to three arguments. The first argument is mandatory: the condition you wish to assert:
private void menuTestDivision_Click( object sender, System.EventArgs e) { Employee joe = new Employee("Joe",47,0); Debug.Assert(joe.YearsOfService != 0);
If you run this code, when the Assert fails (as it will in this case), you are presented with an Assertion failure window, shown in Figure 21-24, which provides a stack trace and the ability to ignore the failure, retry the condition, or abort the program.
Figure 21-24. Assertion failure
You are free to add two more parameters: a short string and a longer description:
Debug.Assert(joe.YearsOfService != 0, "Age can not be zero!", "It was expected that the age of an employee could never be zero");
If you do, the Assert window will display this additional information, as shown circled in Figure 21-25.
Figure 21-25. Assert failedadditional information
21.6.1 Side effects
When using Debug statements, take care to not set any values or otherwise change the state of your program within the Assert statement. When you recompile into Release mode, the Debug Assertions and other statements are removed from your programs, and these side effects will be removed as well, which can result in confusing bugs ("every time I put this in the debugger, it works fine, but when I release the code...").
21.6.1.1 Trace versus Debug
The key difference between Trace and Debug is that Trace is enabled (by default) both in debug and release mode, while Debug is enabled (by default) only in Debug mode. This difference allows you to decide what debugging statements make it into your release code. (Note that Trace statements take up room in your release code even if you disable them, unless you do not compile them into your program by undefining the TRACE symbol.)
21.6.1.2 Trace and Debug methods
The Trace and Debug classes have many useful methods in addition to Assert. Each method is static and void (in C#) or shared sub in VB.NET. The most important methods are listed in Table 21-4.
Method |
Description |
---|---|
Assert |
Overloaded. Tests a condition and displays a message. |
Fail |
Like Assert but doesn't test a conditionjust displays a message. |
Indent |
Increases the indentation level in the output by one (for organizing output). |
Unindent |
Decreases the indentation level by one. |
Write/WriteLine |
Overloaded method that writes extensive debug information to the listeners in the Listeners collection. WriteLine version adds a new line at the end. |
WriteIf/WriteLineIf |
Like Write, but writes only if a condition is met. |
21.6.2 Listeners
A powerful alternative to displaying error messages is capturing errors to log files. To do this you will need to add a Listener to your program.
Listeners can listen to both Debug and Trace classes. You will find three types of predefined listeners:
TextWriterListener
Redirects its output to an instance of the TextWriter class or to any Stream class. This listener can also write to the console or to a file because both are Stream objects.
EventLogListener
Emits Write/WriteLine messages to an Event log.
DefaultTraceListener
Emits Write and WriteLine messages to the OutputDebugString and to the Debugger.Log method. If you want a listener besides (or instead of) the DefaultTraceListener to receive Debug and Trace output, you must explicitly add it to the Listeners collection.
For example, you can change the Test Division code to write to a custom log with these lines of code:
private void menuTestDivision_Click( object sender, System.EventArgs e) { Employee joe = new Employee("Joe",47,0); Trace.Listeners.Clear( ); // clear all listeners Trace.Listeners.Add(new EventLogTraceListener("BugRiddenLog")); Debug.Assert(joe.YearsOfService != 0, "Age can not be zero!", "It was expected that the age of an employee could never be zero"); if ( joe.YearsOfService != 0 ) { Double pctgOfLife = joe.Age / joe.YearsOfService; lblDisplay.Text = joe.Name + " has worked here for " + pctgOfLife + "% of his life!"; } }
Notice that you've encased the actual division in a test so you do not fire an exception. When you run this code, nothing appears to happen. To see the actual log, go to Start
Figure 21-26. Event log