Fluent APIs are the best kind. They allow developers to jump in and use it with very little knowledge. A good fluent API takes you through a guided tour just by using IntelliSense.
One of my favourites is FluentAssertions — you’d have a hard time finding a .NET project that didn’t use it:
Fluent Assertions is a set of .NET extension methods that allow you to more naturally specify the expected outcome of a TDD or BDD-style unit test. This enables a simple intuitive syntax that all starts with the following
using
statement:
Here’s an example snippet of FluentAssertions:
string actual = "ABCDEFGHI";
actual.Should().StartWith("AB").And.EndWith("HI").And.Contain("EF").And.HaveLength(9);
In this blog, I’m going to make my own — primitive — version of the above.
Coding my own FluentAssertions
FluentAssertions brings a lot of extension methods into the global scope, so that’s where I’m going to start. When you type any value, whether it be a string, int, bool, there’ll be IntelliSense tacked onto the end of that for .Should()
and various others that I won’t get into in this blog.
So lets implement a Should()
extension method:
public static Assertion Should<T>(this T value)
{
var beStage = new Assertion();
beStage.Value = value;
return beStage;
}
This method should latch onto all types, in this blog, for simplicity I’m just going to focus on strings.
You’ll notice that I’ve create a new type called Assertion. The Assertion class is going to be the hub for all the various guided stages.
The simplest example I can think of is:
"blar".Should().Be("blar");
So our new type needs to implement an interface which contains the .Be(object value)
method. The naming convention I’m going to use here is IStageNameStage, so IBeStage for this implementation:
public interface IBeStage
{
void Be(object value);
}
And the implementation:
public class Assertion: IBeStage {
public object Value { get; set; }
public void Be(object value) {
if (!Value.Equals(value)) {
throw new Exception($"Expected {Value} to be {value}");
}
}
}
So now we can write tests like the following:
[Test]
public void Should_Not_Error_When_Values_Are_Equal()
{
"blar".Should().Be("blar");
}
[Test]
public void Should_Return_Error_When_Values_Not_Equal()
{
Assert.Throws<Exception>(() => "blar".Should().Be("blar2"));
}
Brilliant, we’ve got a somewhat fluent API. What about if we need to add another assertion, like contains(value)
:
"blar".Should().Contains("bl");
Then it’s just a case of creating a new interface:
public interface IContainsStage
{
void Contains(object value);
}
And adding that to our Assertion class:
public class Assertion: IBeStage {
public object Value { get; set; }
public void Be(object value) {
if (!Value.Equals(value)) {
throw new Exception($"Expected {Value} to be {value}");
}
}
public void Contains(object value) {
if (!Value.ToString().Contains(value.ToString())) {
throw new Exception($"Expected {Value} to contain {value}");
}
}
}
Which means we can write:
[Test]
public void Should_Return_True_When_Contains()
{
"blar".Should().Contains("bl");
}
This is all well and good, but what I really love about these APIs is the ability to chain:
"blar".Should().Contains("bla").And.Be("blar");
So rather than our methods returning a void
we want to return the next stage, which will be And, let’s create an IAndStage
interface:
public interface IBeStage
{
IAndStage Be(object value);
}
public interface IContainsStage
{
IAndStage Contains(object value);
}
public interface IAndStage
{
public FluentAssertions.Assertion And { get; }
}
And implement the new interface in the Assertion class and update the return types:
public class Assertion : IBeStage, IContainsStage, IAndStage
{
public Assertion And => this;
public object Value { get; set; }
public IAndStage Be(object value)
{
if (Value.Equals(value))
{
return this;
}
throw new Exception($"Expected {Value} to be {value}");
}
public IAndStage Contains(object value)
{
if (Value.ToString().Contains(value.ToString()))
{
return this;
}
throw new Exception($"Expected {Value} to contain {value}");
}
}
So now we can write tests like the following:
[Test]
public void Should_Be_Able_To_Connect_Multiple_Statements()
{
"blar".Should().Contains("bla").And.Be("blar");
}
This is the crux of a fluent API, given a method call, we return the specific interface of the next stage. So in my simple example after performing an assertion the next stage is the IAndStage
. If we wanted to add more options after the assertion stage then we’d have to create a somewhat more generic interface to implement — it’s essentially the builder pattern, but allows the writer to guide developers to prevent duff object creation.
I’ve implemented a fair bit more of the API, you can check out my GitHub for appropriate links.
If you liked this blog then please sign up for my newsletter and join an awesome community!
Also, I currently run a completely free Planning Poker website that is gaining quite a lot of traction. If you work in a dev team and do biweekly sprints, give it a go and let me know if there’s anything you’d like added. You can also get involved, the code is completely open-source.
[…] is built using extension methods, it is a fluent API. The same code using Moq would look like the […]