A state diagram is a type of diagram used in order to describe the behavior of an entity. For instance, here is the example of an elevator made with the help of PlantUML
.
The elevator can be in Stopped
or Moving
states, where a transition between them is a result of Next
or Stop
actions. So for an ancient man, it will be pretty easy to get how the elevator works :)
Problem
Having great documentation for growing app is a hard task because it quickly becomes outdated and can lead to disinformation. Recently, I showed how to generate sequence diagrams automatically. Now, I would like to set up self-documenting for an entity that makes use of state pattern. How to do that with minimal afford?
Solution
Let’s look into UserState
example described here. It is a demonstration of the user login process, you may check the source code to have a clue.
The approach is the same as before - you don’t have to add any dependency to your application source code. All you need is logger and tests, the rest PlantUML
will do for you. I’m pretty sure you already have those two.
The idea is using logger to write PlantUML
code. The test runs, as a result, it produces logs, then the logs are captured and converted to state diagram via PlantUML
automatically.
In the example above, it has unit tests and I’m going to adapt them.
[Test]
public void It_should_login()
{
// Given
var user = new User();
// When
user.State.Login("test");
// Then
Assert.That(user.State.HasAccess, Is.True);
Assert.That(user.State, Is.TypeOf<UserIsAuthorized>());
}
Adapted test just captures logs and nothing more.
private ILogger _logger = new LoggerConfiguration()
.WriteTo.Console(outputTemplate: "{Message}")
.MinimumLevel.Debug()
.CreateLogger();
[Test]
public void It_should_login()
{
// Given
var user = new TestUser(_logger);
// When
user.State.Login("test");
// Then
Assert.That(user.State.HasAccess, Is.True);
Assert.That(user.State, Is.TypeOf<UserIsAuthorized>());
}
Where TestUser
is derived from User
in order to capture the state changes and convert it into PlantUML
code. Pay attention that I’m checking _logger.IsEnabled(LogEventLevel.Debug)
it means there will be no performance penalty if this code runs in production.
public class TestUser : User
{
private readonly ILogger _logger;
public TestUser(ILogger logger)
{
_logger = logger;
}
public override void OnStateChanged(UserState prev, UserState next)
{
if (_logger.IsEnabled(LogEventLevel.Debug))
{
var callingMethod = new StackTrace().GetFrame(2).GetMethod();
_logger.Debug($"{prev.GetType().Name} --> {next.GetType().Name} : {callingMethod.Name}");
}
}
}
The logic of capturing logs is in place. Let’s generate the diagram by the logs.
[Test]
public void PlantUml_should_build_state_diagram()
{
// Given
var planUmlCode = new List<string>();
_logger = Substitute.For<ILogger>();
_logger.IsEnabled(LogEventLevel.Debug).Returns(true);
_logger.When(e => e.Debug(Arg.Any<string>())).Do(e => planUmlCode.Add(e.Arg<string>()));
// When
It_should_login();
It_should_show_captcha();
It_should_validate_captcha();
It_should_be_blocked();
It_should_logout();
// Then
var veryFirstState = planUmlCode[0].Substring(0, planUmlCode[0].IndexOf(" --> "));
planUmlCode.Add($"[*] --> {veryFirstState}");
var code = string.Join("\n", planUmlCode.Distinct());
var diagramUrl = new RendererFactory()
.CreateRenderer()
.RenderAsUri(code, OutputFormat.Png);
Console.WriteLine(diagramUrl);
}
The line [*] --> {veryFirstState}
forces PlantUML
to generate state diagram instead of sequence. The rendering itself is done by good friend PlantUml.Net
package. PlantUML
cannot generate ASCII
state diagram correct, well then PNG
or SVG
to the rescue. It is nice to have URL of the diagram, so even if the test is run by Continuous Integration
server, anyone still is able to open the diagram, that is why RenderAsUri
method is used.
Finally, the result is awesome!
See complete example here Gaev.Blog.Examples.SelfDocumentedFSM.