Toby and the complete stab in the dark


During my rather long wait for the plane in Dublin this morning, I had the opportunity to revisit Toby and his company's EDM. Unfortunately, by the time I got to some stories, I was actually in the air with no internet access and as such had to just leave them for now. Regardless, here's an initial stab at the user stories I mentioned a while back (with perhaps a couple additions.) I'm now wondering if these wouldn't be better expressed with Behave# 🙂

(Aside: The code is going up on Codeplex as soon as the project is approved)

[TestFixture]
public class Class1
{
  
//* The administrator can login to the system.
  [Test]
  
public void CanLogIntoSystemAsAnAdministrator()
  {
      
EDMSystem system = new EDMSystem();

      system.LoginAs(
UserType.Administrator);

      
Assert.Equal<UserType>(
             UserType.Administrator, system.CurrentUser);
  }

  
//* The administrator can create a project.
  [Test]
  
public void TheAdministratorCanCreateAProject()
  {
      
EDMSystem system = new EDMSystem();
      system.LoginAs(
UserType.Administrator);

      system.CreateProject(0,
"my first project");

      
Assert.Equal<int>(1, system.ProjectCount);
  }

  
//* Non-administrative users can login to the system.
  [Test]
  
public void CanLogIntoTheSystemAsANonAdministrativeUser()
  {
      
EDMSystem system = new EDMSystem();

      system.LoginAs(
UserType.Bob);

      
Assert.Equal<UserType>(UserType.Bob, system.CurrentUser);
  }

  
//* A user can find a project by number.
  [Test]
  
public void CanFindProjectByNumber()
  {
      
EDMSystem system = new EDMSystem();
      system.LoginAs(
UserType.Administrator);
      system.CreateProject(1,
"my first project");
      system.LoginAs(
UserType.Bob);

      
Project foundProject = system.FindProject(1);

      
Assert.Equal<int>(1, foundProject.Number);
      
Assert.Equal<string>("my first project", foundProject.Name);
  }

  
//* A user can find a project by name.
  [Test]
  
public void CanFindProjectByName()
  {
      
EDMSystem system = new EDMSystem();
      system.LoginAs(
UserType.Administrator);
      system.CreateProject(1,
"my first project");
      system.LoginAs(
UserType.Bob);

      
Project foundProject = system.FindProject("my first project");

      
Assert.Equal<int>(1, foundProject.Number);
      
Assert.Equal<string>("my first project", foundProject.Name);
  }

  
//* A user can add a drawing to the system.
  [Test]
  
public void CanAddADrawingToAProject()
  {
      
EDMSystem system = new EDMSystem();
      system.LoginAs(
UserType.Administrator);
      
Project project = system.CreateProject(1,
                                    "my first project");
      system.LoginAs(
UserType.Bob);

      project.AddDrawing(
"blah");

      
Assert.Equal<int>(1, project.DrawingCount);
  }

  
//* A user can view the list of drawings for a project
  [Test]
  
public void CanViewTheListOfDrawingsForAProject()
  {
      
EDMSystem system = new EDMSystem();
      system.LoginAs(
UserType.Administrator);
      
Project project = system.CreateProject(1,
                                      "my first project");
      project.AddDrawing(
"blah");
      project.AddDrawing(
"blah too");

      
ReadOnlyCollection<Drawing> drawings = project.Drawings;

      
Assert.Equal<int>(2, drawings.Count);
      
Assert.Equal<string>("blah", drawings[0].Name);
      
Assert.Equal<string>("blah too", drawings[1].Name);
  }

  
// A user can filter the list of drawings by ???

  //* A user can enter (or modify) the date when their department (user) received a drawing.
  [Test]
  
public void CanEnterWhenDepartmentReceivedDrawing()
  {
      
EDMSystem system = new EDMSystem();
      system.LoginAs(
UserType.Administrator);
      
Project project = system.CreateProject(1,
                                      "my first project");
      system.LoginAs(
UserType.Bob);
      
Drawing drawing = project.AddDrawing("blah");
      
DateTime referenceDate = new DateTime(1979, 1, 21);

      drawing.WasReceived(system.CurrentUser, referenceDate);

      
Assert.Equal<DateTime>(referenceDate, drawing.Received(system.CurrentUser));
  }

  
// A user cannot modify when a different department's received a drawing.
  [Test]
  
public void CannotEnterWhenAnotherDepartmentReceivedDrawing()
  {
      
EDMSystem system = new EDMSystem();
      system.LoginAs(
UserType.Administrator);
      
Project project = system.CreateProject(1,
                                       "my first project");
      system.LoginAs(
UserType.Bob);
      
Drawing drawing = project.AddDrawing("blah");
      
DateTime referenceDate = new DateTime(1979, 1, 21);
      drawing.WasReceived(system.CurrentUser, referenceDate);

      system.LoginAs(
UserType.Joe);

      
Assert.Throws<InvalidOperationException>(delegate { drawing.WasReceived(UserType.Bob, DateTime.Now); });
  }

  
//* The administrator can edit all the details for a drawing. (??)
  //* A user can add drawings to the system sequentially without having to re-enter all the data each time.

  //* A user can up-rev a drawing (create a new revision by only changing the revision number and dates)
  [Test]
  
public void UserCanUpRevADrawing()
  {
      
EDMSystem system = new EDMSystem();
      system.LoginAs(
UserType.Administrator);
      
Project project = system.CreateProject(1,
                                       "my first project");
      system.LoginAs(
UserType.Bob);
      
Drawing drawing = project.AddDrawing("blah");
      
DateTime referenceDate = new DateTime(2007, 4, 1);

      
Drawing newDrawing = project.UpRevDrawing(drawing,
                                        "A053", referenceDate);

      
Assert.Equal<int>(2, project.DrawingCount);
      
Assert.Equal<Drawing>(newDrawing, project.Drawings[1]);
      
Assert.Equal<string>("A053", newDrawing.RevisionNumber);
  }

  
//* A user can edit the dates for a group of drawings for their department.
  [Test]
  
public void UserCanEnterWhenDepartementReceivedASetOfDrawings()
  {
      
EDMSystem system = new EDMSystem();
      system.LoginAs(
UserType.Administrator);
      
Project project = system.CreateProject(1,
                                      
"my first project");
      system.LoginAs(
UserType.Bob);
      
Drawing drawing = project.AddDrawing("blah");
      
Drawing drawing2 = project.AddDrawing("blah 2");
      
Drawing drawing3 = project.AddDrawing("blah 3");
      
DateTime referenceDate = new DateTime(1979, 1, 21);
      
List<Drawing> drawings =
                       
new List<Drawing>(project.Drawings);

      drawings.ForEach(
delegate(Drawing d)
           { d.WasReceived(system.CurrentUser, referenceDate); });

      
Assert.Equal<DateTime>(referenceDate,
                        drawings[0].Received(system.CurrentUser));
      
Assert.Equal<DateTime>(referenceDate,
                        drawings[1].Received(system.CurrentUser));
      
Assert.Equal<DateTime>(referenceDate, 
                        drawings[2].Received(system.CurrentUser));
  }

  
//* The administrator can archive a project.
  [Test]
  
public void AdministratorCanArchiveAProject()
  {
      
EDMSystem system = new EDMSystem();
      system.LoginAs(
UserType.Administrator);
      
Project project = system.CreateProject(0,
                                      "my first project");

      project.Archive(system.CurrentUser);

      
Assert.True(project.IsArchived);
  }

  
//* The administrator can un-archive a project.
  [Test]
  
public void AdministratorCanUnArchiveAProject()
  {
      
EDMSystem system = new EDMSystem();
      system.LoginAs(
UserType.Administrator);
      
Project project = system.CreateProject(0,
                                      "my first project");
      project.Archive(system.CurrentUser);

      project.UnArchive(system.CurrentUser);

      
Assert.False(project.IsArchived);
  }
}

public class EDMSystem : IUserService
{
  
public EDMSystem()
  {
      projectDictionary =
new Dictionary<int, Project>();
      projectDictionaryByName =
new Dictionary<string, Project>();
  }

  
public UserType CurrentUser
  {
      
get { return currentUser; }
  }

  
public int ProjectCount
  {
      
get { return 1; }
  }

  
public void LoginAs(UserType userType)
  {
      currentUser = userType;
  }

  
public Project CreateProject(int projectNumber,
                               string projectName)
  {
      
Project project = new Project(projectNumber,
                                    projectName,
this);
      projectDictionary.Add(projectNumber, project);
      projectDictionaryByName.Add(projectName, project);

      
return project;
  }

  
public Project FindProject(int projectNumber)
  {
      
return projectDictionary[projectNumber];
  }

  
public Project FindProject(string projectName)
  {
      
return projectDictionaryByName[projectName];
  }

  
private UserType currentUser;
  
private Dictionary<int, Project> projectDictionary;
  
private Dictionary<string, Project> projectDictionaryByName;
}

public class Project
{
  
public Project(int number, string name,
                
IUserService userService)
  {
      
this.number = number;
      
this.name = name;
      
this.userService = userService;

      drawings =
new List<Drawing>();
  }

  
public string Name
  {
      
get { return name; }
  }

  
public int Number
  {
      
get { return number; }
  }

  
public int DrawingCount
  {
      
get { return drawings.Count; }
  }

  
public ReadOnlyCollection<Drawing> Drawings
  {
      
get { return drawings.AsReadOnly(); }
  }

  
public bool IsArchived
  {
      
get { return isArchived; }
  }

  
public Drawing AddDrawing(string drawingName)
  {
      
Drawing drawing = new Drawing(drawingName, userService);
      drawings.Add(drawing);
      
return drawing;
  }

  
public Drawing UpRevDrawing(Drawing drawing,
                             
string revisionNumber,
                              
DateTime referenceDate)
  {
      
Drawing upRevvedDrawing = new Drawing(
                                   drawing.Name, userService);
      upRevvedDrawing.RevisionNumber = revisionNumber;

      drawings.Add(upRevvedDrawing);

      
return upRevvedDrawing;
  }

  
public void Archive(UserType userType)
  {
      
if (userType == UserType.Administrator)
      {
          isArchived =
true;
      }
  }

  
public void UnArchive(UserType userType)
  {
      
if (userType == UserType.Administrator)
      {
          isArchived =
false;
      }
  }

  
private string name;
  
private int number;
  
private List<Drawing> drawings;
  
private IUserService userService;
  
public bool isArchived;
}

public class Drawing
{
  
public Drawing(string name, IUserService userService)
  {
      
this.name = name;
      
this.userService = userService;

      receivedByUser =
new Dictionary<UserType, DateTime>();
  }

  
public string Name
  {
      
get { return name; }
  }

  
public string RevisionNumber
  {
      
get { return revisionNumber; }
      
set { revisionNumber = value; }
  }

  
public void WasReceived(UserType userType,
                          DateTime referenceDate)
  {
      
if (userType != userService.CurrentUser)
      {
          
throw new InvalidOperationException();
      }

      receivedByUser[userType] = referenceDate;
  }

  
public DateTime Received(UserType userType)
  {
      
return receivedByUser[userType];
  }

  
private string name;
  
private Dictionary<UserType, DateTime> receivedByUser;
  
private IUserService userService;
  
private string revisionNumber;
}

public enum UserType
{
  Administrator,
  Bob,
  Joe
}

public interface IUserService
{
  
UserType CurrentUser { get; }
}

 

There are a few things I want to point out about this code:

  • It's already gone through a few refactorings. When I reached the story about disallowing users to modify other users (groups?) received date I had to introduce IUserSerivce and change a few constructors.
  • I realise my constructors can be changed with Dependency Injection from ObjectBuilder (CAB) when we reach that stage. For now, we're doing it the simple way.
  • There is no real storage mechanism. The user stories never mentioned persistence 🙂
  • The ubiquitous language is starting to change. The user stories are starting to change. This is a Good Thing. One of the conversions I had at Agile 2007 with the Brian Button (an incredibly intelligent and most definitely non-red individual) was about developing bottom-up with EDD versus inside-out. Along with the concepts of BDD, I'm going to try out what he introduced me to. Brian - apologies if I'm not doing things quite right .. I'm learning as I go along here 🙂

I'd love to hear what people think about this .. have I completely gone off the deep end with regards to EDD here? Are the unit tests I currently have better written as acceptance tests (in Behave# for instance)? Any and all comments welcome.

Comments (3)
  1. Brian Button says:

    Hi, Casper,

    Interesting to watch your classes evolve out of your tests here. One thing that I see is that you’re pretty tightly coupled to the idea that the EDMSystem has to do everything for you. Each test seems to start out by creating a bit more infrastructure for you.

    Is it possible to split out some of this stuff into smaller fixtures with common setup/teardown logic? Both your Drawing and your Project class have non-trivial code in them that you had to have driven through tests, but I only see the tests for the EDMSystem. I generally don’t write a test in the fixture for one class that forces me to write code in a different class. When I want to do that, it is a sign that I have to write another fixture for that class directly. I think that doing so might take some of the ever-increasing complexity out of your EDMSystem tests.

    I think the change in your process that you made is that you didn’t design the Project and Drawing classes first, right? You let the tests for the EDMSystem drive out their requirements and then wrote them?

    If that was it, then I say that you did a nice job of it. Like we discussed, when I go bottom-up, I generally build abstractions that work on their own, but don’t really match the context where they’re going to end up being used. This style that you’ve used here should prevent that from happening.

    Nice job!

    bab

  2. Casper says:

    Thanks for the comments Brian. As you pointed out, this isn’t the way I normally do things so I’m still feeling my way around bit by bit 🙂

    • I did not design the Project and Drawing classes first .. they were driven out of the features I needed to get a story written. So there are no tests for them, which is a bad thing. In this case, once I hit the need for a Project class, you’re saying that it would be best to stop working in the EDMSytem fixture, start a Project fixture, and then alternate between the two as the need arises?

    • I agree that the EDMSystem is definitely doing too many things. Looking at its current state, it seems to handle user management (login for now), and project-related functions (create and find). I’m guessing that the latter could be moved into a separate Project ‘service’ of some sort. Do you see any further breakdowns?

  3. Brian Button says:

    #1 – What I’m saying is that your EDMSystem tests should define the requirements for your Project and Drawing classes, but they shouldn’t drive code into those classes. When you find yourself writing an example for EDMSystem that is going to require you to write code in Project or Drawing, then you can either mock out the method in the current test to get that test to pass, which will allow you to flesh out how the mocked out method should work, or you can stop and write tests in the other fixture, as you say, and then come back to this test.

    I don’t have a preference between the two approaches, as I use both of them at the right time. I guess the right time is decided by whether the original test is just too big to implement, which would force me to put it down for now, or whether I just need to add a bit of functionality to the other class before finishing the first test.

    #2 – Nothing jumped out at me yet, but there isn’t that much there yet. Write more code and we’ll see where this goes.

    bab

Comments are closed.

Skip to main content