- The Interface
- Implementation
- Building a Window
- The Payroll Window
- The Unveiling
- Conclusion
- Bibliography
Building a Window
Designing the Add Employee window GUI code was a breeze. With Visual Studio's designer, it's simply a matter of dragging some controls around to the right place. This code is generated for us and is not included in the following listings. Once the window is designed, we have more work to do. We need to implement some behavior in the UI and wire it to the presenter. We also need a test for it all. Listing 38-5 shows AddEmployeeWindowTest, and Listing 38-6 shows AddEmployeeWindow.
Listing 38-5. AddEmployeeWindowTest.cs
using NUnit.Framework; namespace PayrollUI { [TestFixture] public class AddEmployeeWindowTest { private AddEmployeeWindow window; private AddEmployeePresenter presenter; private TransactionContainer transactionContainer; [SetUp] public void SetUp() { window = new AddEmployeeWindow(); transactionContainer = new TransactionContainer(null); presenter = new AddEmployeePresenter( window, transactionContainer, null); window.Presenter = presenter; window.Show(); } [Test] public void StartingState() { Assert.AreSame(presenter, window.Presenter); Assert.IsFalse(window.submitButton.Enabled); Assert.IsFalse(window.hourlyRateTextBox.Enabled); Assert.IsFalse(window.salaryTextBox.Enabled); Assert.IsFalse(window.commissionSalaryTextBox.Enabled); Assert.IsFalse(window.commissionTextBox.Enabled); } [Test] public void PresenterValuesAreSet() { window.empIdTextBox.Text = "123"; Assert.AreEqual(123, presenter.EmpId); window.nameTextBox.Text = "John"; Assert.AreEqual("John", presenter.Name); window.addressTextBox.Text = "321 Somewhere"; Assert.AreEqual("321 Somewhere", presenter.Address); window.hourlyRateTextBox.Text = "123.45"; Assert.AreEqual(123.45, presenter.HourlyRate, 0.01); window.salaryTextBox.Text = "1234"; Assert.AreEqual(1234, presenter.Salary, 0.01); window.commissionSalaryTextBox.Text = "123"; Assert.AreEqual(123, presenter.CommissionSalary, 0.01); window.commissionTextBox.Text = "12.3"; Assert.AreEqual(12.3, presenter.Commission, 0.01); window.hourlyRadioButton.PerformClick(); Assert.IsTrue(presenter.IsHourly); window.salaryRadioButton.PerformClick(); Assert.IsTrue(presenter.IsSalary); Assert.IsFalse(presenter.IsHourly); window.commissionRadioButton.PerformClick(); Assert.IsTrue(presenter.IsCommission); Assert.IsFalse(presenter.IsSalary); } [Test] public void EnablingHourlyFields() { window.hourlyRadioButton.Checked = true; Assert.IsTrue(window.hourlyRateTextBox.Enabled); window.hourlyRadioButton.Checked = false; Assert.IsFalse(window.hourlyRateTextBox.Enabled); } [Test] public void EnablingSalaryFields() { window.salaryRadioButton.Checked = true; Assert.IsTrue(window.salaryTextBox.Enabled); window.salaryRadioButton.Checked = false; Assert.IsFalse(window.salaryTextBox.Enabled); } [Test] public void EnablingCommissionFields() { window.commissionRadioButton.Checked = true; Assert.IsTrue(window.commissionTextBox.Enabled); Assert.IsTrue(window.commissionSalaryTextBox.Enabled); window.commissionRadioButton.Checked = false; Assert.IsFalse(window.commissionTextBox.Enabled); Assert.IsFalse(window.commissionSalaryTextBox.Enabled); } [Test] public void EnablingAddEmployeeButton() { Assert.IsFalse(window.submitButton.Enabled); window.SubmitEnabled = true; Assert.IsTrue(window.submitButton.Enabled); window.SubmitEnabled = false; Assert.IsFalse(window.submitButton.Enabled); } [Test] public void AddEmployee() { window.empIdTextBox.Text = "123"; window.nameTextBox.Text = "John"; window.addressTextBox.Text = "321 Somewhere"; window.hourlyRadioButton.Checked = true; window.hourlyRateTextBox.Text = "123.45"; window.submitButton.PerformClick(); Assert.IsFalse(window.Visible); Assert.AreEqual(1, transactionContainer.Transactions.Count); } } }
Listing 38-6. AddEmployeeWindow.cs
using System; using System.Windows.Forms; namespace PayrollUI { public class AddEmployeeWindow : Form, AddEmployeeView { public System.Windows.Forms.TextBox empIdTextBox; private System.Windows.Forms.Label empIdLabel; private System.Windows.Forms.Label nameLabel; public System.Windows.Forms.TextBox nameTextBox; private System.Windows.Forms.Label addressLabel; public System.Windows.Forms.TextBox addressTextBox; public System.Windows.Forms.RadioButton hourlyRadioButton; public System.Windows.Forms.RadioButton salaryRadioButton; public System.Windows.Forms.RadioButton commissionRadioButton; private System.Windows.Forms.Label hourlyRateLabel; public System.Windows.Forms.TextBox hourlyRateTextBox; private System.Windows.Forms.Label salaryLabel; public System.Windows.Forms.TextBox salaryTextBox; private System.Windows.Forms.Label commissionSalaryLabel; public System.Windows.Forms.TextBox commissionSalaryTextBox; private System.Windows.Forms.Label commissionLabel; public System.Windows.Forms.TextBox commissionTextBox; private System.Windows.Forms.TextBox textBox2; private System.Windows.Forms.Label label1; private System.ComponentModel.Container components = null; public System.Windows.Forms.Button submitButton; private AddEmployeePresenter presenter; public AddEmployeeWindow() { InitializeComponent(); } protected override void Dispose( bool disposing ) { if( disposing ) { if(components != null) { components.Dispose(); } } base.Dispose( disposing ); } #region Windows Form Designer generated code // snip #endregion public AddEmployeePresenter Presenter { get { return presenter; } set { presenter = value; } } private void hourlyRadioButton_CheckedChanged( object sender, System.EventArgs e) { hourlyRateTextBox.Enabled = hourlyRadioButton.Checked; presenter.IsHourly = hourlyRadioButton.Checked; } private void salaryRadioButton_CheckedChanged( object sender, System.EventArgs e) { salaryTextBox.Enabled = salaryRadioButton.Checked; presenter.IsSalary = salaryRadioButton.Checked; } private void commissionRadioButton_CheckedChanged( object sender, System.EventArgs e) { commissionSalaryTextBox.Enabled = commissionRadioButton.Checked; commissionTextBox.Enabled = commissionRadioButton.Checked; presenter.IsCommission = commissionRadioButton.Checked; } private void empIdTextBox_TextChanged( object sender, System.EventArgs e) { presenter.EmpId = AsInt(empIdTextBox.Text); } private void nameTextBox_TextChanged( object sender, System.EventArgs e) { presenter.Name = nameTextBox.Text; } private void addressTextBox_TextChanged( object sender, System.EventArgs e) { presenter.Address = addressTextBox.Text; } private void hourlyRateTextBox_TextChanged( object sender, System.EventArgs e) { presenter.HourlyRate = AsDouble(hourlyRateTextBox.Text); } private void salaryTextBox_TextChanged( object sender, System.EventArgs e) { presenter.Salary = AsDouble(salaryTextBox.Text); } private void commissionSalaryTextBox_TextChanged( object sender, System.EventArgs e) { presenter.CommissionSalary = AsDouble(commissionSalaryTextBox.Text); } private void commissionTextBox_TextChanged( object sender, System.EventArgs e) { presenter.Commission = AsDouble(commissionTextBox.Text); } private void addEmployeeButton_Click( object sender, System.EventArgs e) { presenter.AddEmployee(); this.Close(); } private double AsDouble(string text) { try { return Double.Parse(text); } catch (Exception) { return 0.0; } } private int AsInt(string text) { try { return Int32.Parse(text); } catch (Exception) { return 0; } } public bool SubmitEnabled { set { submitButton.Enabled = value; } } } }
Despite all my griping about how painful it is to test GUI code, testing Windows Form code is relatively easy. There are some pitfalls, however. For some silly reason, known only to programmers at Microsoft, half of the functionality of the controls does not work unless they are displayed on the screen. It is for this reason that you'll find the call window.Show() in the SetUp of the test fixture. When the tests are executed, you can see the window appearing and quickly disappearing for each test. This is annoying but bearable. Anything that slows down the tests or otherwise makes them clumsy makes it more likely that the tests will not be run.
Another limitation is that you cannot easily invoke all the events on a control. With buttons and buttonlike controls, you can call PerformClick, but events such as MouseOver, Leave, Validate, and others are not so easy. An extension for NUnit, called NUnitForms, can help with these problems and more. Our tests are simple enough to get by without extra help.
In the SetUp of our test, we create an instance of AddEmployeeWindow and give it an instance of AddEmployeePresenter. Then in the first test, StartingState, we make sure that several controls are disabled: hourlyRateTextBox, salaryTextBox, commissionSalaryTextBox, and commissionTextBox. Only one or two of these fields are needed, and we don't know which ones until the user chooses the payment type. To avoid confusing the user by leaving all the fields enabled, they'll remain disabled until needed. The rules for enabling these controls are specified in three tests: EnablingHourlyFields, EnablingSalaryField, and EnablingCommissionFields. EnablingHourlyFields, for example, demonstrates how the hourlyRateTextBox is enabled when the hourlyRadioButton is turned on and disabled when the radio button is turned off. This is achieved by registering an EventHandler with each RadioButton. Each EventHandler enables and disables the appropriate text boxes.
The test, PresenterValuesAreSet, is an important one. The presenter know what to do with the data, but it's the view's responsibility to populate the data. Therefore, whenever a field in the view is changed, it calls the corresponding property on the presenter. For each TextBox in the form, we use the Text property to change the value and then check to make sure that the presenter is properly updated. In AddEmployeeWindow, each TextBox has an EventHandler registered on the TextChanged event. For the RadioButton controls, we call the PerformClick method in the test and again make sure that the presenter is informed. The RadioButton's EventHandlers take care of this.
EnablingAddEmployeeButton specifies how the submitButton is enabled when the SubmitEnabled property is set to true, and reverse. Remember that in AddEmployeePresenterTest, we didn't care what this property did. Now we do care. The view must respond properly when the SubmitEnabled property is changed; however, AddEmployeePresenterTest was not the right place to test it. AddEmployee-WindowTest focuses on the behavior of the AddEmployeeWindow, and it is the right place to test this unit of code.
The final test here is AddEmployee, which fills in a valid set of fields, clicks the Submit button, asserts that the window is no longer visible, and makes sure that a transaction was added to the transactionContainer. To make this pass, we register an EventHandler, on the submitButton, that calls AddEmployee on the presenter and then closes the window. If you think about it, the test is doing a lot of work just to make sure that the AddEmployee method was called. It has to populate all the fields and then check the transactionContainer. Some might argue that instead, we should use a mock presenter so we can easily check that the method was called. To be honest, I wouldn't put up a fight if my pair partner were to bring it up. But the current implementation doesn't bother me too much. It's healthy to include a few high-level tests like this. They help to make sure that the pieces can be integrated properly and that the system works the way it should when put together. Normally, we'd have a suite of acceptance tests that do this at an even higher level, but it doesn't hurt to do it a bit in the unit tests—just a bit, though.
With this code in place, we now have a working form for creating AddEmployeeTransactions. But it won't get used until we have the main Payroll window working and wired up to load our AddEmployeeWindow.