as part of my
The Markdown Format
The Markdown format has rapidly grown in popularity over the last 10 years. Originally conceived by John Gruber as a quick way to produce HTML markup, it has spread within the developer community to become a popular standard for producing online content.
I first heard of Markdown from Jeff Atwood's blog post when he was building StackOverflow. Since then, many apps and web pages have appeared that use the format, including GitHub, Discourse, as well as some excellent editors, such as iA Writer, MarkdownPad and the browser-based Dillinger.
It is easy to see why Markdown has become popular. Compare the difference between raw HTML and Markdown:
Not only is the Markdown code easier to write, it's easier to read too. And, it can easily be converted to HTML using one of the many parsers, or apps. I'm using one of the apps, MarkdownPad, to produce this entire article.
Producing Markdown-formatted Test Results
I develop a free shopping list app for iOS. There are many other apps that help you remember what to buy in the supermarket, but I thought the world needed another shopping list app when I couldn't find one that worked how I wanted. I wanted a shopping list app that would offer suggestions as I typed, and to automatically arrange my list based on the layout of the supermarket.
Writing a Test Suite
I recently made some major changes to the app's suggestion and classification engine, based on user feedback from the first version.
Before making changes, I built a suite of tests that I could run during development to check I hadn't broken anything or hurt performance - basically, to protect the app from my own fallibility.
Unit Tests
In general, a unit test will test a very specific behaviour and return a PASS
/FAIL
result.
For example,
Assert.AreEqual(3, Math.Sqrt(9));
Assert.AreEqual(Double.NaN, Math.Sqrt(-1));
Unit tests are great for testing individual methods and classes that have well specified behaviour. I added loads of these to my suite but, to give me the extra confidence I craved, I also needed subcutaneous tests.
Subcutaneous Integration Tests
A subcutaneous test is used to show how the app performs and behaves at a level immediately below the user interface - just under its skin.
These tests would need to operate on real data, on the actual device, to produce a simulation of what users would experience. Each test would have a number of steps, corresponding to a sequence of interactions between the user and the device.
For example,
- User types
br
- Expect
bread
to be amongst the suggestions offered
- User selects
bread
from the list of suggestions
- User enters Shopping Mode
- Expect
bread
to be categorised in the Bakery
section
To make writing these tests easier, I built a framework so tests could be written concisely, using a fluent coding style. This was written using the same tools as the app - C#, .NET and Xamarin - which meant it could run directly on the iPhone and produce accurate, real-world timings.
Here's an example of a finished test:
tests.Run(app => app
.Type("2 small onions").PressEnterKey()
.Type("5 kilos of potatoes").PressEnterKey()
.Type("br")
.ExpectThat(x => x.SuggestionsInclude("bread"))
.SelectSuggestion("bread")
.StartShopping()
.ExpectThat(x => x.CategoryContains("Bakery", new[] {"bread"}))
.ExpectThat(x => x.CategoryContains("Fresh Fruit, Veg & Flowers",
new[]
{
"2 small onions",
"5 kilos of potatoes"
})),
synopsis: "Can select suggestion and categorise");
As the test runs, it records each user action, along with relevant observations, results and timing information.
This is where Markdown comes in.
As part of my testing framework, I built a library called MarkdownLog, which I've now released as open-source under the MIT license. This library is designed to make it trivial to produce Markdown formatted text from an application's data structures. Using just one line of code, a collection of .NET objects can be output as a table or list. And, because the output is Markdown, it can later be converted to HTML for publishing, if needed.
Here's an extract of a Markdown formatted log from a test run:
Summary of Test Suite Run
=========================
Suite completed on 17 July 2014 at 09:52:04 (UTC)
3 tests (223 actions) passed in **422** ms
Result | Expected Behaviour | Actions | Time Taken
------:| ------------------------------------------- | -------:| ----------:
PASS | Can select suggestion and categorise | 36 | 199
PASS | Common products are included in suggestions | 75 | 153
PASS | Common brands are included in suggestions | 112 | 80
Environment
===========
Key | Value
----------------- | ------------------------------------
HardwareModel | iPhone
SystemName | iPhone OS
SystemVersion | 7.1
AppVersion | 1.01
DeviceIdForVendor | ---
Test 1: Can select suggestion and categorise
============================================
Completed in **199** ms
*Note: Some output has been removed from here for brevity*
Action: Type 'b'
----------------
Suggestions are shown:
-------------
| bread |
| beans |
| breakfast > |
-------------
Action: Type 'r'
----------------
Suggestions are shown:
-------------
| bread |
| breakfast > |
| British > |
-------------
Check: Suggestions list should include 'bread'
----------------------------------------------
PASS: 'bread' was present in the list of suggestions
Action: Choose 'bread' from suggestions list
--------------------------------------------
The shopping list now contains:
-----------------------
| 2 small onions |
| 5 kilos of potatoes |
| bread |
>| |
-----------------------
Action: Enter 'Shopping' mode
-----------------------------
Shopping mode has been entered.
Items have been categorised:
--------------------------
|Fresh Fruit, Veg & Flowers|
|--------------------------|
| [ ] 2 small onions |
| [ ] 5 kilos of potatoes |
|--------------------------|
|Bakery |
|--------------------------|
| [ ] bread |
--------------------------
Check: Section 'Bakery' should contain 'bread'
----------------------------------------------
PASS: 'bread' was found in 'Bakery' section
Check: Section 'Fresh Fruit, Veg & Flowers' should contain: '2 small onions', '5 kilos of potatoes'
---------------------------------------------------------------------------------------------------
PASS: '2 small onions' was found in 'Fresh Fruit, Veg & Flowers' section
PASS: '5 kilos of potatoes' was found in 'Fresh Fruit, Veg & Flowers' section
Test 1 Timings (ms)
===================
Typing '2' |### 3
Typing ' ' |### 3
Typing 's' |### 3
Typing 'm' |###### 6
Typing 'a' |################################ 32
Typing 'l' |##### 5
Typing 'l' |# 1
Typing ' ' |## 2
Typing 'o' |## 2
Typing 'n' |### 3
Typing 'i' |######### 9
Typing 'o' |## 2
Typing 'n' |# 1
Typing 's' |############### 15
Typing '5' | 0
Typing ' ' |# 1
Typing 'k' |### 3
Typing 'i' |# 1
Typing 'l' |######## 8
Typing 'o' |## 2
Typing 's' | 0
Typing ' ' |# 1
Typing 'o' |### 3
Typing 'f' |#### 4
Typing ' ' |# 1
Typing 'p' |#### 4
Typing 'o' |### 3
Typing 't' |##### 5
Typing 'a' |### 3
Typing 't' |# 1
Typing 'o' |# 1
Typing 'e' |######################################### 41
Typing 's' |## 2
Typing 'b' |# 1
Typing 'r' |# 1
Categorising items |################### 19
------------------------------------------
Overall Slowest Timings
=======================
Note: timings are classified based on human perception metrics from [Jacob Nielson's article](http://www.nngroup.com/articles/powers-of-10-time-scales-in-ux/):
* Anything that takes less than 0.1 seconds feels IMMEDIATE - as if the user
is directly causing something to happen.
* Between 0.1 and 1 second is quick enough to give the user MAINTAINED-FOCUS
and feel in control.
* Longer than 1 second but less than 10 seconds is acceptable for
MAINTAINED-ATTENTION, but the user may begin to feel impatient.
* Generally, BROKEN-FLOW, will occur after 10 seconds and the user will
context switch and do something else.
Test | Action | Perception | Time (ms)
------------------------------------------- | ------------------ | ---------- | ---------
Can select suggestion and categorise | Typing 'e' | IMMEDIATE | 41
Can select suggestion and categorise | Typing 'a' | IMMEDIATE | 32
Common products are included in suggestions | Typing 'g' | IMMEDIATE | 28
Can select suggestion and categorise | Categorising items | IMMEDIATE | 19
Common products are included in suggestions | Typing 'c' | IMMEDIATE | 19
Common products are included in suggestions | Typing 'c' | IMMEDIATE | 19
Can select suggestion and categorise | Typing 's' | IMMEDIATE | 15
Common products are included in suggestions | Typing 'w' | IMMEDIATE | 13
Common products are included in suggestions | Typing 'b' | IMMEDIATE | 12
Common products are included in suggestions | Typing 'b' | IMMEDIATE | 10
View as HTML
As you can see, the Markdown formatted log is very readable in its raw form so the output can be written to the Console and viewed immediately, without the need for special viewers or converters.
MarkdownLog
MarkdownLog is designed to be lightweight and simple to use. It is a .NET Portable Class Library (PCL) with no dependencies, besides the .NET Framework.
Getting Started
- Download MarkdownLog.dll from the build server.
- Add a reference to MarkdownLog.dll from your project.
- Add
using MarkdownLog;
to the top of your file.
- Produce Markdown output from your data.
MarkdownLog can produce all the elements described in the Markdown spec.
Create a List from a Collection
var myStrings = new[] { "John", "Paul", "Ringo", "George" };
Console.Write(myStrings.ToMarkdownBulletedList());
This produces:
* John
* Paul
* Ringo
* George
View as HTML
The collection can be of any type, not just strings. The ToString
method will be called on each object to produce the list item’s text. You can also specify a function, like this:
var processes = Process.GetProcesses();
Console.Write(processes.ToMarkdownBulletedList(i => i.ProcessName));
If you'd prefer a numbered list, use the ToMarkdownNumberedList
extension method instead:
var files = new DirectoryInfo(@"C:\MarkdownLog").EnumerateFiles();
Console.Write(files.ToMarkdownNumberedList(i => i.Name + " is " + i.Length + " bytes"));
This produces:
1. appveyor.yml is 302 bytes
2. LICENSE is 1088 bytes
3. MarkdownLog.sln is 1480 bytes
4. README.md is 6173 bytes
View as HTML
Create a Table
A collection of data can be output as a GitHub Flavoured Markdown Table using the ToMarkdownTable
extension method.
var data = new[]
{
new{Name = "Meryl Streep", Nominations = 18, Awards=3},
new{Name = "Katharine Hepburn", Nominations = 12, Awards=4},
new{Name = "Jack Nicholson", Nominations = 12, Awards=3}
};
Console.Write(data.ToMarkdownTable());
This produces:
<code>
Name | Nominations | Awards
----------------- | -----------:| ------:
Meryl Streep | 18 | 3
Katharine Hepburn | 12 | 4
Jack Nicholson | 12 | 3
</code>
View as HTML
The columns are aligned automatically based on datatype. Text is left-aligned and numbers are right-aligned.
The columns to be included, and how their data is selected, can be specified using a method overload. Simply pass a function for each of the columns you need:
var tableWithHeaders = data
.ToMarkdownTable(i => i.Name, i => i.Nominations + i.Awards)
.WithHeaders("Name", "Total");
Console.Write(tableWithHeaders);
This produces:
<code>
Name | Total
----------------- | -----:
Meryl Streep | 21
Katharine Hepburn | 16
Jack Nicholson | 15
</code>
Create a Bar Chart
A bar chart can be created using a collection of labels and numbers.
var worldCup = new Dictionary<string, int>
{
{"Brazil", 5},
{"Italy", 4},
{"Germany", 4},
{"Argentina", 2},
{"Uruguay", 2},
{"France", 1},
{"Spain", 1},
{"England", 1}
};
Console.Write(worldCup.ToMarkdownBarChart());
This is rendered as a Markdown code block:
Brazil |##### 5
Italy |#### 4
Germany |#### 4
Argentina |## 2
Uruguay |## 2
France |# 1
Spain |# 1
England |# 1
------
View as HTML
Bar charts support negative and floating point numbers, and scaling of values too:
Cos(0.0) |#################### 1
Cos(0.3) |################### 0.95
Cos(0.6) |################ 0.81
Cos(0.9) |############ 0.59
Cos(1.3) |###### 0.31
Cos(1.6) | 0
Cos(1.9) ######| -0.31
Cos(2.2) ############| -0.59
Cos(2.5) ################| -0.81
Cos(2.8) ###################| -0.95
Cos(3.1) ####################| -1
Cos(3.5) ###################| -0.95
Cos(3.8) ################| -0.81
Cos(4.1) ############| -0.59
Cos(4.4) ######| -0.31
Cos(4.7) | 0
Cos(5.0) |###### 0.31
Cos(5.3) |############ 0.59
Cos(5.7) |################ 0.81
Cos(6.0) |################### 0.95
-----------------------------------------
View as HTML
Create a Header
Console.Write("The Origin of the Species".ToMarkdownHeader());
This produces:
The Origin of the Species
=========================
View as HTML
Create a Sub-header
Console.Write("By Means of Natural Selection".ToMarkdownSubHeader());
This produces:
By Means of Natural Selection
-----------------------------
View as HTML
Create a Word-Wrapped Paragraph
var text = "Lolita, light of my life, fire of my loins." +
"My sin, my soul. Lo-lee-ta: the tip of the tongue taking a trip of" +
"three steps down the palate to tap, at three, on the teeth. Lo. Lee. Ta.";
Console.Write(text.ToMarkdownParagraph());
This produces:
Lolita, light of my life, fire of my loins. My sin, my soul. Lo-lee-ta: the tip
of the tongue taking a trip of three steps down the palate to tap, at three, on
the teeth. Lo. Lee. Ta.
View as HTML
Paragraphs are word-wrapped at the 80th column, by default, but this can be configured.
Create a Block-quote
const string text = "There are only two hard things in computer science:\n" +
"cache invalidation,\n" +
"naming things,\n" +
"and off-by-one errors.";
Console.Write(text.ToMarkdownBlockquote());
This produces:
> There are only two hard things in computer science:
> cache invalidation,
> naming things,
> and off-by-one errors.
View as HTML
A block-quote can also contain other Markdown elements. For example:
var blockQuote = new Blockquote();
blockQuote.Append(new HorizontalRule());
blockQuote.Append(new Header("COMPUTING MACHINERY AND INTELLIGENCE"));
blockQuote.Append(new SubHeader("By A. M. Turing."));
blockQuote.Append(new Paragraph("..."));
blockQuote.Append(new Paragraph("The idea behind digital computers" +
"may be explained by saying that these machines are intended to carry" +
"out any operations which could be done by a human computer. The human" +
"computer is supposed to be following fixed rules; he has no authority" +
"to deviate from them in any detail. We may suppose that these rules" +
"are supplied in a book, which is altered whenever he is put on to a" +
"new job. He has also an unlimited supply of paper on which he does his" +
"calculations. He may also do his multiplications and additions on a" +
"\"desk machine,\" but this is not important."));"
blockQuote.Append(new Paragraph("If we use the above explanation as" +
"a definition we shall be in danger of circularity of argument. We avoid" +
"this by giving an outline. of the means by which the desired effect is" +
"achieved. A digital computer can usually be regarded as consisting of three parts:"));
blockQuote.Append(new NumberedList("Store", "Executive unit", "Control"));
Console.Write(blockQuote);
This produces:
> --------------------------------------------------------------------------------
>
> COMPUTING MACHINERY AND INTELLIGENCE
> ====================================
>
> By A. M. Turing.
> ----------------
>
> ...
>
> The idea behind digital computers may be explained by saying that these
> machines are intended to carry out any operations which could be done by a
> human computer. The human computer is supposed to be following fixed rules; he
> has no authority to deviate from them in any detail. We may suppose that these
> rules are supplied in a book, which is altered whenever he is put on to a new
> job. He has also an unlimited supply of paper on which he does his
> calculations. He may also do his multiplications and additions on a "desk
> machine," but this is not important.
>
> If we use the above explanation as a definition we shall be in danger of
> circularity of argument. We avoid this by giving an outline. of the means by
> which the desired effect is achieved. A digital computer can usually be
> regarded as consisting of three parts:
>
> 1. Store
> 2. Executive unit
> 3. Control
>
View as HTML
Deferring Output by Writing to a Container
So far, we've been writing each element directly to the Console window as we go. This can be useful for producing a snapshot of a specific data structure to the output window while an application is running. However, to produce a complete log, which can be saved to disk, each element should be written to a MarkdownContainer
.
var log = new MarkdownContainer();
var countries = new[]{"Zimbabwe", "Italy", "Bolivia", "Finland", "Australia"};
log.Append("Countries (unsorted)".ToMarkdownHeader());
log.Append(countries.ToMarkdownNumberedList());
var sorted = countries.OrderBy(i => i);
log.Append("Countries (sorted)".ToMarkdownHeader());
log.Append(sorted.ToMarkdownNumberedList());
Console.Write(log);
This produces:
Countries (unsorted)
====================
1. Zimbabwe
2. Italy
3. Bolivia
4. Finland
5. Australia
Countries (sorted)
==================
1. Australia
2. Bolivia
3. Finland
4. Italy
5. Zimbabwe
View as HTML
If you'd like to learn more about MarkdownLog, please visit the Project Homepage, where you'll find a few more examples and a link to the latest source code. If you're interested in contributing, I'd very much like to hear from you!
And, if you happen to be looking for a free app for iPhone that saves you time with your weekly shop, please take a look at Shopping UK and let me know what you think :)