So this post, we'll actually wire up specs to a view. First though, in the last paragraph of my last post I said that the presenter wouldn't save a person w/ the same name as another person in the system. That's not true, the Presenter's IsValid property returns false, but since the presenter doesn't check the property, it saves it anyway. Lets go fix that real quick.
So the original test was this
[Test]
public void you_can_not_save_2_people_with_the_same_name()
{
var person1 = new Person {Name = "Some Dude"};
allPeople.Add(person1);
var person2 = new Person {Name = "Some Other Dude"};
allPeople.Add(person2);
repository.Stub(r => r.GetAll()).Return(allPeople);
repository.Stub(r => r.GetById(person1.Id)).Return(person1);
view.Stub(v => v.SelectedPersonId).Return(person1.Id);
// now set the views name to the name of the other person
view.Stub(v => v.Name).Return(person2.Name);
Act();
view.Raise(v => v.PersonSelected += null,null,EventArgs.Empty);
Assert.IsFalse(presenter.Valid);
}
Note that it only checks the presenter.Valid property. That's not really a good test... Who cares what the property says, the fact that I wrapped a protected method shoulda sang of smell to me when I did it... Oh well, Lets change it.
1: [Test]
2: public void you_can_not_save_2_people_with_the_same_name()
3: {
4:
5: var person1 = new Person {Name = "Some Dude"};
6: allPeople.Add(person1);
7: var person2 = new Person {Name = "Some Other Dude"};
8: allPeople.Add(person2);
9: repository.Stub(r => r.GetAll()).Return(allPeople);
10: repository.Stub(r => r.GetById(person1.Id)).Return(person1);
11:
12: view.Stub(v => v.SelectedPersonId).Return(person1.Id);
13: // now set the views name to the name of the other person
14: view.Stub(v => v.Name).Return(person2.Name);
15:
16: Act();
17: view.Raise(v => v.PersonSaved += null, null, EventArgs.Empty);
18:
19: repository.AssertWasNotCalled(r => r.Save(null),options => options.IgnoreArguments());
20:
21:
22: }
Not THAT fails... So lets go fix it. Really really easy....
1: private void PersistPerson(object sender, EventArgs e)
2: {
3: var selectedPerson = repository.GetById(View.SelectedPersonId);
4: selectedPerson.Name = View.Name;
5: if(!IsValid) return;
6: repository.Save(selectedPerson);
7: HydrateAllPeople();
8: }
Line 5 is all we did. Check that IsValid property. If it returns false, we don't save. Yep... the test passes.
Ok, so now that we've fixed that bug, lets go actually communicate to the user why when they clicked save, the person didn't get saved.
All we need to do is drop a ValidationMessageControl on the page. First the Register directive
<%@ Register Assembly="MvpFramework" Namespace="MvpFramework" TagPrefix="mvp" %>
and then the control.
1: <div>
2: <asp:Label runat="server" AssociatedControlID="NameControl" Text="Name" />
3: <asp:TextBox runat="server" ID="NameControl" />
4: <span style="color:#F00; font-weight:bold">
5: <mvp:ValidationMessage runat="server" ID="NameMustBeUnique" Text="2 people can't have the same name!" />
6: </span>
7: </div>
Now we've gotta go set the SpecificationType property, for now, you've gotta do this in the codebehind. Hopefully I'll make that unnecessary soon. But even like this it's not bad.
Here's the change.
1: protected override void OnInit(EventArgs e)
2: {
3: base.OnInit(e);
4: AllPeopleControl.SelectedIndexChanged += (o, ev) => PersonSelected(o, ev);
5: AddNewButton.Click += (o, ev) => PersonAdded(o, ev);
6: SaveExistingButton.Click += (o, ev) => PersonSaved(o, ev);
7: NameMustBeUnique.SpecificationType = typeof (CanNotHaveSameNameSpecification);
8: }
We're interested in line 7. Just set the type and whenever the IsValid is called on the presenter, if the spec fails, the Text is shown.
Lets give it a test run. After adding a few "people"...
I'll go select one of them.
And save him as "Test" since I know that name is already taken.
And vola... It just works....
Let's go make a new requirement. Say, the Name has to have at least 5 characters in it.
1: [Test]
2: public void a_person_can_not_be_added_if_the_name_is_less_than_5_characters()
3: {
4: view.Stub(v => v.Name).Return("fail");
5: Act();
6: view.Raise(v => v.PersonAdded += null,null,EventArgs.Empty);
7:
8: repository.AssertWasNotCalled(r => r.Save(null),options => options.IgnoreArguments());
9: }
Of course it fails...
Let's go write up the spec.
1: using MvpFramework.Specifications;
2:
3: namespace Demo.Presentation
4: {
5: public class NameMustBeLongerThan5CharactersSpecification:Specification<IEditPersonView>
6: {
7: public override bool IsSatisfiedBy(IEditPersonView obj)
8: {
9: return obj.Name.Length > 5;
10: }
11: }
12: }
And run the test...
But it fails?!
So, I knew this would happen, but I figured I'd let it play out. Being the idiot that I am, I never checked IsValid for Adding... Only for saving..
In fact, I bet this will fail too.
1: [Test]
2: public void a_person_with_the_same_name_of_one_that_already_exists_can_not_be_added()
3: {
4: var testPerson = new Person {Name = "Test Person"};
5: allPeople.Add(testPerson);
6: repository.Stub(r => r.GetAll()).Return(allPeople);
7: view.Stub(v => v.Name).Return(testPerson.Name);
8:
9: Act();
10: view.Raise(v => v.PersonAdded += null,null,EventArgs.Empty);
11:
12: repository.AssertWasNotCalled(r => r.Save(null),options => options.IgnoreArguments());
13: }
So yeah, we just add a person to the repo, and try to add another with the same name. Sure enough, it fails too (I'll save the pic, trust me, it fails). Let's go fix 'em both.
1: private void SaveANewPerson(object sender, EventArgs e)
2: {
3: var person = new Person {Name = View.Name};
4: if(!IsValid) return;
5: repository.Save(person);
6: HydrateAllPeople();
7: }
And vola! They pass. Actually, I had to go make a few other unit test changes, since I was persisting people with no name in some tests. Here's the whole fixture after tests.
1: using System;
2: using System.Collections.Generic;
3: using Demo.Business;
4: using Demo.Presentation;
5: using NUnit.Framework;
6: using Rhino.Mocks;
7: using StructureMap;
8:
9: namespace Demo.UnitTests.Presentation
10: {
11: [TestFixture]
12: public class When_editing_a_person
13: {
14: private IPersonRepository repository;
15: private List<Person> allPeople;
16: private IEditPersonView view;
17: private EditPersonPresenter presenter;
18:
19: [SetUp]
20: public void Arrange()
21: {
22: repository = MockRepository.GenerateMock<IPersonRepository>();
23: allPeople = new List<Person>();
24: view = MockRepository.GenerateMock<IEditPersonView>();
25: SetupStructureMap();
26:
27: }
28:
29: private void SetupStructureMap()
30: {
31: ObjectFactory.Initialize(
32: registry => registry.Scan(x =>
33: {
34: x.AssemblyContainingType<IPersonRepository>();
35: registry.ForRequestedType<IPersonRepository>().TheDefault.Is.Object(
36: repository);
37:
38: }));
39:
40: }
41:
42: public void Act()
43: {
44: repository.Stub(r => r.GetAll()).Return(allPeople);
45: presenter = new EditPersonPresenter(view, repository);
46: }
47: [Test]
48: public void all_people_is_set_to_all_people_from_the_repo_on_initial_load()
49: {
50: view.Stub(v => v.IsInitialLoad).Return(true);
51: Act();
52:
53: view.AssertWasCalled(v => v.AllPeople = allPeople);
54: }
55: [Test]
56: public void when_a_person_is_selected_the_view_is_bound_to_it()
57: {
58: //Add a person to the repo
59: var person = new Person {Name = "Test Person"};
60: allPeople.Add(person);
61: view.Stub(v => v.SelectedPersonId).Return(person.Id);
62: repository.Stub(r => r.GetById(person.Id)).Return(person);
63: Act();
64: view.Raise(v => v.PersonSelected += null,null,EventArgs.Empty);
65:
66: view.AssertWasCalled(v => v.Name = person.Name);
67: }
68: [Test]
69: public void when_the_user_clicks_save_the_person_is_persisted()
70: {
71: //Add a person to the repo
72: var person = new Person { Name = "Test Person" };
73: allPeople.Add(person);
74: repository.Stub(r => r.GetById(person.Id)).Return(person);
75:
76: view.Stub(v => v.SelectedPersonId).Return(person.Id);
77: var theName = "Modified";
78: view.Stub(v => v.Name).Return(theName);
79: Act();
80: view.Raise(v => v.PersonSaved += null,null,EventArgs.Empty);
81:
82: var personSentToRepo = (Person)repository.GetArgumentsForCallsMadeOn(r => r.Save(person))[0][0];
83: Assert.AreEqual(person.Id,personSentToRepo.Id);
84: Assert.AreEqual(theName,personSentToRepo.Name);
85:
86: }
87: [Test]
88: public void you_can_not_save_2_people_with_the_same_name()
89: {
90:
91: var person1 = new Person {Name = "Some Dude"};
92: allPeople.Add(person1);
93: var person2 = new Person {Name = "Some Other Dude"};
94: allPeople.Add(person2);
95: repository.Stub(r => r.GetById(person1.Id)).Return(person1);
96:
97: view.Stub(v => v.SelectedPersonId).Return(person1.Id);
98: // now set the views name to the name of the other person
99: view.Stub(v => v.Name).Return(person2.Name);
100:
101: Act();
102: view.Raise(v => v.PersonSaved += null, null, EventArgs.Empty);
103:
104: repository.AssertWasNotCalled(r => r.Save(null),options => options.IgnoreArguments());
105:
106:
107: }
108: [Test]
109: public void AllPeople_is_not_set_if_the_view_is_a_postback()
110: {
111: view.Stub(v => v.IsInitialLoad).Return(false);
112: Act();
113: view.AssertWasNotCalled(v => v.AllPeople = allPeople);
114: }
115: [Test]
116: public void when_PersonAdded_is_raised_a_new_person_is_added_to_the_repo()
117: {
118: var theNewName = "The New Person";
119: view.Stub(v => v.Name).Return(theNewName);
120:
121: Act();
122: view.Raise(v => v.PersonAdded += null,null,EventArgs.Empty);
123:
124: var thePersonSaved = (Person)repository.GetArgumentsForCallsMadeOn(r => r.Save(null), o => o.IgnoreArguments())[0][0];
125: Assert.AreEqual(theNewName,thePersonSaved.Name);
126: }
127: [Test]
128: public void when_a_person_is_added_AllPersons_is_repopulated()
129: {
130: view.Stub(v => v.Name).Return("Some Guy");
131: Act();
132: view.Raise(v => v.PersonAdded += null,null,EventArgs.Empty);
133: view.AssertWasCalled(v=> v.AllPeople = allPeople);
134: }
135: [Test]
136: public void when_a_person_is_saved_AllPeople_is_updated()
137: {
138: var someDude = new Person {Name = "Some Dude"};
139: allPeople.Add(someDude);
140: repository.Stub(r => r.GetById(someDude.Id)).Return(someDude);
141: view.Stub(v => v.SelectedPersonId).Return(someDude.Id);
142: view.Stub(v => v.Name).Return("Edited");
143: Act();
144: view.Raise(v => v.PersonSaved+= null,null,EventArgs.Empty);
145: view.AssertWasCalled(v => v.AllPeople = allPeople);
146: }
147: [Test]
148: public void a_person_can_not_be_added_if_the_name_is_less_than_5_characters()
149: {
150: view.Stub(v => v.Name).Return("fail");
151: Act();
152: view.Raise(v => v.PersonAdded += null,null,EventArgs.Empty);
153:
154: repository.AssertWasNotCalled(r => r.Save(null),options => options.IgnoreArguments());
155: }
156: [Test]
157: public void a_person_with_the_same_name_of_one_that_already_exists_can_not_be_added()
158: {
159: var testPerson = new Person {Name = "Test Person"};
160: allPeople.Add(testPerson);
161: view.Stub(v => v.Name).Return(testPerson.Name);
162:
163: Act();
164: view.Raise(v => v.PersonAdded += null,null,EventArgs.Empty);
165:
166: repository.AssertWasNotCalled(r => r.Save(null),options => options.IgnoreArguments());
167: }
168: }
169: }
The fact that I had to fix some tests brings up another point. All validation for presenters is delegated to a PresenterValidator object. When using the framework, if I have a lot of validation that takes place, I'll usually just inject a mocked validator and stub out IsValid so I don't have to mock up the view to pass all specs. I'll discuss this in depth in some future blog. Anyway, let's go hit the UI again.
1: <div>
2: <asp:Label runat="server" AssociatedControlID="NameControl" Text="Name" />
3: <asp:TextBox runat="server" ID="NameControl" />
4: <span style="color:#F00; font-weight:bold">
5: <mvp:ValidationMessage runat="server" ID="NameMustBeUnique" Text="2 people can't have the same name!" />
6: <mvp:ValidationMessage runat="server" ID="NameMustBeMoreThan5Characters" Text="The name must be at least 6 characters" />
7: </span>
8: </div>
And also, set the specificationtype for the new validation message.
1: protected override void OnInit(EventArgs e)
2: {
3: base.OnInit(e);
4: AllPeopleControl.SelectedIndexChanged += (o, ev) => PersonSelected(o, ev);
5: AddNewButton.Click += (o, ev) => PersonAdded(o, ev);
6: SaveExistingButton.Click += (o, ev) => PersonSaved(o, ev);
7: NameMustBeUnique.SpecificationType = typeof (CanNotHaveSameNameSpecification);
8: NameMustBeMoreThan5Characters.SpecificationType = typeof (NameMustBeLongerThan5CharactersSpecification);
9: }
Ok, let's test drive it... Save some dude named "s".
And it works...
So there you have it folks.. How to use validation with the MvpFramework. Hope it helps everyone.
Next up is Navigation....
Happy coding..