In the previous posting, I set up the ProjectTracker solution, including the xUnit testing folders. We talk a lot about test-driven design, but the truth is that we (most of us?) start with an overall architecture in mind prior to writing anything.
For ProjectTracker, I know I’m going to have a project model, and I know that it will contain a name and a way to get the total duration of work on the project, as well as “segments” which will hold each “chunk” of work.
I also know that I’ll have multiple projects but that at most one will be active at a time.
Now, the specifics of how to put that into code… well that I’m going to determine test-first:
The first test is to see if, when I start tracking the project becomes active.
[Fact]
public void StartTrackingSetsActive() {
var project = new Project("Name");
Assert.False(project.IsActive);
project.StartTracking(DateTime.UtcNow);
Assert.True(project.IsActive);
}
There are a lot of implications in this test, but nothing I’ve not noted above. There are two asserts in one test, but that is okay with me since they are tightly coupled.
Red-Green-Refactor says that we should run this test to make sure it fails (it does). It has to as we haven’t written the code yet to make it pass. Let’s do that, remembering that we’re only going to write “just enough” code to get the test to pass. This avoids YAGNI (You Ain’t Gonna’ Need It) problems.
I’ve created a Project class in my Model folder. In there, I have this code:
public string Name { get; }
public Project(string name) {
Name = name;
}
private bool isActive = false;
public bool IsActive {
get { return isActive; }
set {
isActive = value;
RaisePropertyChanged(nameof(IsActive));
}
}
public void StartTracking(DateTime startTimeUtc) {
IsActive = true;
}
Now, I know there are other properties I’ll need, and StartTracking has much more to do, but one thing at a time. With this code in place, my test passes. I can, if I want, now refactor this code and use the tests to prove I’ve not broken anything.
You can assume I’m using Red-Green-Refactor for the rest of the tests. Let’s see how they evolve.
Building on the test above, I’ve got this test:
[Fact]
public void StopTrackingSetsInactive() {
var project = new Project("Name");
project.StartTracking(DateTime.UtcNow);
project.StopTracking(DateTime.UtcNow);
Assert.False(project.IsActive);
}
That means I need logic for StopTracking,
public void StopTracking(DateTime stopTimeUtc) {
IsActive = false;
}
Again, we’re keeping things painfully simple.
Let’s add one more test for today. We want to have a way to test the total duration of a project, and we know projects are going to consist of segments. We’ll have a method Tick that will take a DateTime representing the endTime. For segments that don’t have an end time we’ll use the current time. The purpose of this work is to set a property: TotalDurationSeconds.
public void Tick(DateTime utcNow) {
TotalDurationSeconds =
(int)segments.Sum(segment =>
(segment.EndTimeUtc ?? utcNow)
.Subtract(segment.StartTimeUtc)
.TotalSeconds);
}
Knowing we have that code, we can create the next test,
[Fact]
public void TickSegmentInProgressDurationBasedOnCurrentTime() {
var startingTime = DateTime.UtcNow;
var secondsToAdvance = 10;
var project = new Project("Name");
Assert.Equal(0, project.TotalDurationSeconds);
project.StartTracking(startingTime);
project.Tick(startingTime.AddSeconds(secondsToAdvance));
Assert.Equal(secondsToAdvance, project.TotalDurationSeconds);
}
You can see that we now must create the segments and the property TotalDurationSeconds. This test will then test that the segment that is currently running will have its duration based on the current time (hence the name).
Let’s jazz up Project.cs with some additional code,
public class Project : BindableObjectBase {
public string Name { get; }
private readonly IList<Segment> segments = new List<Segment>();
private Segment activeSegment;
public Project(string name) {
Name = name;
}
private int totalDurationSeconds = 0;
public int TotalDurationSeconds {
get { return totalDurationSeconds; }
set {
totalDurationSeconds = value;
RaisePropertyChanged(nameof(TotalDurationSeconds));
}
}
private bool isActive = false;
public bool IsActive {
get { return isActive; }
set {
isActive = value;
RaisePropertyChanged(nameof(IsActive));
}
}
public void StartTracking(DateTime startTimeUtc) {
if (activeSegment != null) {
StopTracking(startTimeUtc);
}
activeSegment = new Segment(startTimeUtc);
segments.Add(activeSegment);
IsActive = true;
}
public void StopTracking(DateTime stopTimeUtc) {
if (activeSegment == null) {
return;
}
activeSegment.EndTimeUtc = stopTimeUtc;
activeSegment = null;
IsActive = false;
}
public void Tick(DateTime utcNow) {
TotalDurationSeconds =
(int)segments.Sum(segment =>
(segment.EndTimeUtc ?? utcNow)
.Subtract(segment.StartTimeUtc)
.TotalSeconds);
}
All of this code falls out of the few Unit Tests we’ve looked at so far. Next week we’ll continue to build on our program, focusing more on the ViewModel and its tests.
Once again, please note that this series would be impossible without the tremendous help of Greg Shackles.