Steps:
- Intro
- VSO
- Docker
- xUnit
- Build
- Back end
- Selenium
- Docker
- Release Management
- Testing
In this post, we are going to complete the test project we
created in the previous posts. We are
going to use Selenium as the test framework to allow us to test the UI during
our release.
Before we can test our application, we need it to actually
do something. In the post where we created all the Docker hosts, we only had a
“Hello World” ASP.NET 5 application. We
created the backend in the last post. In
this post we will create the functionality and create the tests. This is a very long post so, to speed you up,
I created a repo on
GitHub
with each step of this blog series.
To begin we are going to add a test row to our database.
Then we are going to deploy our Web API into the Azure Web Apps we created
previously. Finally, we will complete
the ASP.NET 5 application. Please note I
will not be going into great detail on how ASP.NET 5 works; this post is long
enough as it is.
1.
Set Id column of Database to Identity
a.
Right-click the dbo.People table in SQL Server
Object Explorer and select View Designer
b.
Right-click the Id roll and select Properties
c.
Change Identity Specification to True
d.
Click Update
e.
Click Update Database
2.
Add test row
a.
Right-click the dbo.People table in SQL Server
Object Explorer and select View Data
b.
Enter a Test User row
3.
Publish your Web API to Azure
a.
Open the PeopleTrackerWebService solution
b.
Right-click PeopleTracker.NetService and select
Publish
c.
Select Microsoft Azure Web Apps
d.
Select the Web App we created for our dev
environment
e.
Click OK
f.
Click Next
g.
Select Release for Configuration
h.
Verify connection string is correct
i.
Check the check box to use this connection
string at runtime
j.
Click Next
k.
Click Publish
i.
Once
the publish is complete a browser will open with our Web API loaded.
ii.
Copy
the URL and paste it somewhere you can get to. You are going to need this URL
later.
4.
Test Web API
a.
In the browser that opened after you published
the Web API project append api/people to the address
b.
A json file will be returned with our test user
in it.
With the Web API deployed, we can now finish our front end. Buckle up boys and girls because this is
going to get crazy. If you do not want
to copy and paste the code from this post, you can clone
this repo
instead.
You will want to switch to branch Step7.
What we are about to do is write all the code that will implement the
CRUD operations for our person object.
Instead of accessing the database directly we are going to use the Web
API that we just deployed. We are going
to define an interface and use the repository pattern to hide the details from
our controller class so we can switch the data access layer in the future. Because we have multiple environments, we have
to have a way to configure the URL for the Web API for each host. We are going to use the new
Options
feature
of ASP.NET5 for this purpose.
5.
Add Options Class
a.
Open PeopleTracker Solution
b.
Right-click on the PeopleTracker.Preview project
and select Add / Class…
c.
Name the file SiteOptions.cs
d.
Replace the contents of the file with the code
below
namespace PeopleTracker.Preview
{
public class SiteOptions
{
/// <summary>
/// Stores the URL of the Web API
/// </summary>
public string WebApiBaseUrl { get; set; }
/// <summary>
/// Stores the build number running in this image
/// </summary>
public string BuildNumber { get; set; }
}
}
6.
Add Person Model
a.
Create a Models folder in the
PeopleTracker.Preview project
b.
Right-click on the Models folder and select Add
/ Class…
c.
Name the file Person.cs
d.
Replace the contents of the file with the codeu
below
namespace PeopleTracker.Preview.Models
{
using System.ComponentModel.DataAnnotations;
public class Person
{
public int ID { get; set; }
[Required]
public string FirstName { get; set; }
[Required]
public string LastName { get; set; }
}
}
7.
Add Repository Interface
a.
Right-click on the Models folder and select Add
/ New Item…
b.
Select Interface and name the file
IRepository.cs
c.
Replace the contents of the file with the code
below
namespace PeopleTracker.Preview.Models
{
using System.Collections.Generic;
public interface IRepository
{
IEnumerable<Person> People { get;
}
bool
AddPerson(Person person);
void
RemovePerson(Person person);
bool
UpdatePerson(Person person);
}
}
8.
Add References to System.Net.Http, Microsoft.Net.Http,
and Microsoft.AspNet.WebApi.Client
a.
Right-click PeopleTracker.Preview project and
select Manage NuGet Packages…
b.
Uncheck the Include prerelease checkbox
c.
Add the following packages to your project
·
System.net.http
·
Microsoft.AspNet.WebApi.Client
·
Microsoft.Net.Http
b.
Open project.json
c.
Move the Microsoft.AspNet.WebApi.Client and
Microsoft.Net.Http from dnx451 section to the root dependencies section
9.
Add Repository Class
a.
Right-click on the Models folder and select Add
/ Class…
b.
Name the file Repository.cs
c.
Replace the contents of the file with the code
below
namespace PeopleTracker.Preview.Models
{
using
Microsoft.Framework.OptionsModel;
using
System;
using
System.Collections.Generic;
using
System.Net.Http;
using
System.Net.Http.Headers;
public class Repository
:
IRepository
{
private IOptions<SiteOptions
>
siteOptions;
public
Repository(
IOptions<SiteOptions> options)
{
this
.siteOptions
= options;
}
public IEnumerable<Person
>
People
{
get
{
var
client = GetClient();
var
response = client.GetAsync(
"api/People").Result;
if
(response.IsSuccessStatusCode)
{
return
response.Content.ReadAsAsync<
IEnumerable<Person>>().Result;
}
return
new Person[0];
}
}
public bool AddPerson(Person
person)
{
var
client = GetClient();
var
response = client.PostAsJsonAsync<
Person>("api/People", person).Result;
return
response.IsSuccessStatusCode;
}
public void RemovePerson(Person
person)
{
var
client = GetClient();
// If you
don't wait you will return to the list page before the item
// is
removed.
client.DeleteAsync("api/People/"
+ person.ID).Wait();
}
public bool UpdatePerson(Person
person)
{
var
client = GetClient();
var
response = client.PutAsJsonAsync<
Person>("api/People/"
+ person.ID,
person).Result;
return response.IsSuccessStatusCode;
}
private HttpClient GetClient()
{
HttpClient
client =
new HttpClient();
client.BaseAddress = new Uri(siteOptions.Value.WebApiBaseUrl);
// Add an
Accept header for JSON format.
client.DefaultRequestHeaders.Accept.Add(
new
MediaTypeWithQualityHeaderValue("application/json"));
return
client;
}
}
}
10.
Add People Controller to PeopleTracker.Preview
a.
Right-click on the Controllers folder and select
Add / Class…
b.
Name the file PeopleController.cs
c.
Replace the contents of the file with the code
below
namespace PeopleTracker.Preview.Controllers
{
using
Microsoft.AspNet.Mvc;
using
Microsoft.Framework.OptionsModel;
using
PeopleTracker.Preview.Models;
using
System.Linq;
using
System.Net;
public class PeopleController
:
Controller
{
private IRepository db;
private IOptions<SiteOptions
>
siteOptions;
public
PeopleController(
IRepository repo, IOptions<SiteOptions
>
options)
{
this
.db
= repo;
this
.siteOptions
= options;
}
public IActionResult Create()
{
ViewData["WebApiBaseUrl"
]
=
this.siteOptions?.Value.WebApiBaseUrl;
return
this.View();
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create([Bind("ID", "FirstName"
,
"LastName")] Person person)
{
ViewData["WebApiBaseUrl"
]
=
this.siteOptions.Value.WebApiBaseUrl;
if (this.ModelState.IsValid)
{
this.db.AddPerson(person);
return
this.RedirectToAction("Index");
}
return
View(person);
}
public ActionResult Delete(int
?
id)
{
ViewData["WebApiBaseUrl"
]
=
this.siteOptions.Value.WebApiBaseUrl;
if
(id
==
null)
{
return
new HttpStatusCodeResult((int)HttpStatusCode.BadRequest);
}
var
person =
this
.db.People.First(p => p.ID ==
id);
if
(person ==
null)
{
return
this.HttpNotFound();
}
return
View(person);
}
[HttpPost]
[ActionName("Delete")]
[ValidateAntiForgeryToken]
public ActionResult DeleteConfirmed(int id)
{
ViewData["WebApiBaseUrl"
]
=
this.siteOptions.Value.WebApiBaseUrl;
var
person =
this
.db.People.FirstOrDefault(p =>
p.ID == id);
if
(person ==
null)
{
return
this.HttpNotFound();
}
this.db.RemovePerson(person);
return
this.RedirectToAction("Index");
}
public ActionResult Details(int
?
id)
{
ViewData["WebApiBaseUrl"
]
=
this.siteOptions.Value.WebApiBaseUrl;
if
(id
==
null)
{
return
new HttpStatusCodeResult((int)HttpStatusCode.BadRequest);
}
var
person =
this
.db.People.FirstOrDefault(p =>
p.ID == id);
if
(person ==
null)
{
return
this.HttpNotFound();
}
return
View(person);
}
public ActionResult Edit(int
?
id)
{
ViewData["WebApiBaseUrl"
]
=
this.siteOptions.Value.WebApiBaseUrl;
if
(id
==
null)
{
return
new HttpStatusCodeResult((int)HttpStatusCode.BadRequest);
}
var
person =
this
.db.People.FirstOrDefault(p =>
p.ID == id);
if
(person ==
null)
{
return
this.HttpNotFound();
}
return
View(person);
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit([Bind("ID", "FirstName"
,
"LastName")] Person person)
{
ViewData["WebApiBaseUrl"
]
=
this.siteOptions.Value.WebApiBaseUrl;
if (this.ModelState.IsValid)
{
this.db.UpdatePerson(person);
return
this.RedirectToAction("Index");
}
return
View(person);
}
public IActionResult Index()
{
ViewData["WebApiBaseUrl"
]
=
this.siteOptions.Value.WebApiBaseUrl;
return
this.View(db.People.ToList());
}
}
}
11.
Add the Create People View
a.
Add a People folder in the Views folder
b.
Right-click the People folder and select Add /
New Item
c.
Select MVC View Page
d.
Name the file Create.cshtml
e.
Replace the contents of the file with the code
below
@model PeopleTracker.Preview.Models.Person
@{
ViewBag.Title = "Create";
}
<h2>Create</h2>
<form asp-controller="People" asp-action="Create" method="post">
<div class="form-horizontal">
<h4>Person</h4>
<hr />
<div asp-validation-summary="ValidationSummary.All"></div>
<div class="form-group">
<label asp-for="FirstName"
class
="control-label
col-md-2"></
label>
<div class="col-md-10">
<input asp-for="FirstName"
/>
<span asp-validation-for="FirstName"></span>
</div>
</div>
<div class="form-group">
<label asp-for="LastName"
class
="control-label
col-md-2"></
label>
<div class="col-md-10">
<input asp-for="LastName"
/>
<span asp-validation-for="LastName"></span>
</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" value="Create" class="btn btn-default" />
</div>
</div>
</div>
</form>
<div>
<a asp-action="Index">Back to List</a>
</div>
12.
Add the Delete People View
a.
Add a People folder in the Views folder
b.
Right-click the People folder and select Add /
New Item
c.
Select MVC View Page
d.
Name the file Delete.cshtml
e.
Replace the contents of the file with the code
below
@model PeopleTracker.Preview.Models.Person
@{
ViewBag.Title = "Delete";
}
<h2>Delete</h2>
<h3>
Are you sure you want to delete
this?
</h3>
<div>
<h4>Person</h4>
<hr />
<dl class="dl-horizontal">
<dt><label asp-for="FirstName"></label></dt>
<dd>@Model.FirstName</dd>
<dt><label asp-for="LastName"></label></dt>
<dd>@Model.LastName</dd>
</dl>
<form asp-controller="People" asp-action="Delete" method="post" asp-route-id="@Model.ID">
<div class="form-actions no-color">
<button type="submit"
class
="btn
btn-default">
Delete</button> |
<a asp-action="Index">Back to List</a>
</div>
</form>
</div>
13.
Add the Details People View
a.
Add a People folder in the Views folder
b.
Right-click the People folder and select Add /
New Item
c.
Select MVC View Page
d.
Name the file Details.cshtml
e.
Replace the contents of the file with the code
below
@model PeopleTracker.Preview.Models.Person
@{
ViewBag.Title = "Details";
}
<h2>Details</h2>
<div>
<h4>Person</h4>
<hr />
<dl class="dl-horizontal">
<dt><label asp-for="LastName"></label></dt>
<dd>@Model.FirstName</dd>
<dt><label asp-for="FirstName"></label></dt>
<dd>@Model.LastName</dd>
</dl>
</div>
<p>
<a asp-action="Edit" asp-route-id="@Model.ID" asp-controller="People">Edit</a>
|
<a asp-action="Index">Back to List</a>
</p>
14.
Add the Edit People View
a.
Add a People folder in the Views folder
b.
Right-click the People folder and select Add /
New Item
c.
Select MVC View Page
d.
Name the file Edit.cshtml
e.
Replace the contents of the file with the code
below
@model PeopleTracker.Preview.Models.Person
@{
ViewBag.Title = "Edit";
}
<h2>Edit</h2>
<form asp-controller="People" asp-action="Edit" method="post" asp-route-id="@Model.ID">
<div class="form-horizontal">
<h4>Person</h4>
<hr />
<div asp-validation-summary="ValidationSummary.All"></div>
<input type="hidden" asp-for="ID" />
<div class="form-group">
<label asp-for="FirstName"
class
="control-label
col-md-2"></
label>
<div class="col-md-10">
<input asp-for="FirstName"
/>
<span asp-validation-for="FirstName"></span>
</div>
</div>
<div class="form-group">
<label asp-for="LastName"
class
="control-label
col-md-2"></
label>
<div class="col-md-10">
<input asp-for="LastName"
/>
<span asp-validation-for="LastName"></span>
</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" value="Save" class="btn btn-default" />
</div>
</div>
</div>
</form>
<div>
<a asp-action="Index">Back to List</a>
</div>
15.
Add the Index People View
a.
Add a People folder in the Views folder
b.
Right-click the People folder and select Add /
New Item
c.
Select MVC View Page
d.
Name the file Index.cshtml
e.
Replace the contents of the file with the code
below
@model IEnumerable<PeopleTracker.Preview.Models.Person>
@{
ViewBag.Title = "People";
}
<h2>Index</h2>
<p>
<a asp-controller="People" asp-action="Create">Create New</a>
</p>
<table class="table">
<tr>
<th>
@
Html.DisplayNameFor(model
=> model.FirstName)
</th>
<th>
@
Html.DisplayNameFor(model
=> model.LastName)
</th>
<th></th>
</tr>
@foreach
(var item in
Model)
{
<tr>
<td>
@item.FirstName
</td>
<td>
@item.LastName
</td>
<td>
<a asp-action="Edit" asp-route-id="@item.ID">Edit</a> |
<a asp-action="Details" asp-route-id="@item.ID">Details</a> |
<a asp-action="Delete" asp-route-id="@item.ID">Delete</a>
</td>
</tr>
}
</table>
16.
Add People to Main Menu
a.
Open _Layout.cshtml
b.
Add the following code to the navbar-nav ul
<li><a asp-controller="People" asp-action="Index">People</a></li>
17.
Configure Services
a.
Open Startup.cs
b.
Copy and paste this code below
services.AddMvc(); in ConfigureServices
services.AddOptions();
services.Configure<SiteOptions>(Configuration);
services.AddScoped<IRepository, Repository>();
c.
Use the light bulb to add any required usings
18.
Update appsettings.json
Depending on when you started
following this series, you may have a config.json or an appsettings.json. The goal is to add values to the file
configured with the ConfigurationBuilder in the Startup class.
a.
Open appsettings.json (or config.json depending
on your version of ASP.NET5)
b.
Replace the contents of the file with the code
below
{
"WebApiBaseUrl"
:
"http://localhost:52593/",
"BuildNumber"
:
"1"
}
c.
Update the base url to point to your dev
instance of the Web API
19.
Update copyright with base url and build number
a.
Open HomeController.cs
b.
Replace the contents of the file with the code
below
namespace PeopleTracker.Preview.Controllers
{
using
Microsoft.AspNet.Mvc;
using
Microsoft.Framework.OptionsModel;
public class HomeController
:
Controller
{
private IOptions<SiteOptions
>
siteOptions;
public
HomeController(
IOptions<SiteOptions> options)
{
this
.siteOptions
= options;
}
public IActionResult Index()
{
SetCopyright();
return
View();
}
private void SetCopyright()
{
ViewData["WebApiBaseUrl"
]
=
string.Format(
"{0}
- build: {1}"
, this
.siteOptions.Value.WebApiBaseUrl,
this.siteOptions.Value.BuildNumber);
}
public IActionResult About()
{
ViewData["Message"
]
=
"People Tracker is a demo application that
shows the power of Microsoft DevOps."
;
SetCopyright();
return
View();
}
public IActionResult Contact()
{
ViewData["Message"
]
=
"Follow me on Twitter to stay connected to
Microsoft DevOps."
;
SetCopyright();
return
View();
}
public IActionResult Error()
{
SetCopyright();
return
View(
"~/Views/Shared/Error.cshtml");
}
}
}
20.
Update _Layout.cshtml to show build and base url
a.
Open _Layout.cshtml
b.
Replace your copyright paragraph with the code below
<p>© 2015 - PeopleTracker.Preview - @ViewData["WebApiBaseUrl"]</p>
Adding the dependency on site options to the HomeController
class will break our unit tests we added earlier. We need to update the tests so they will
build and pass.
21.
Add FakeOptions class
a.
Right-click on PeopleTracker.Preview.Tests
project and select Add / Class
b.
Name the file FakeOptions.cs
c.
Replace the contents of the file with the code
below
namespace PeopleTracker.Preview.Tests
{
using
Microsoft.Framework.OptionsModel;
public class FakeOptions
:
IOptions<SiteOptions>
{
private SiteOptions _value;
public
FakeOptions(
SiteOptions value)
{
_value = value;
}
public SiteOptions Value
{
get
{
return
_value;
}
}
}
}
22.
Update unit test
a.
Open HomeControllerTests.cs
b.
Replace the contents of the file with the code
below
namespace PeopleTracker.Preview.Tests
{
using Xunit;
public class HomeControllerTests
{
[Fact]
public void Index()
{
// Arrange
var
target =
new PeopleTracker.Preview.Controllers.HomeController(new
FakeOptions(new
SiteOptions()));
// Act
var
results = target.Index();
// Assert
Assert.NotNull(results);
}
}
}
At this point, you should have a fully functional ASP.NET 5
People Tracker web site. Now we can use
this web site to record our Selenium tests.
23.
Add Selenium references
a.
Open PeopleTrackerWebService Solution
b.
Right-click on your UI test project and select
Manage NuGet Packages…
c.
Add the following packages to your project
·
Selenium.WebDriver
·
Selenium.Support
·
Selenium.WebDriver.ChromeDriver
·
Selenium.WebDriver.IEDriver
I
installed only Chrome and IE drivers because the Firefox driver is part of the
core framework and does not have to be installed separately. The drivers are how I select the browser I
want to use to execute my test.
The tests have to be written in such a way that we do not
hardcode the URL of the site being tested. This way we can pass in the desired
URL during test execution during our release.
We are also going to use the Test Category attribute so we can
categorize Unit tests and UI tests. This will allow us to only run UI tests
during our release.
24.
Write test case
a.
Open UnitTest1.cs
b.
Replace the contents of the file with the code
below
namespace PeopleTracker.UITests
{
using
Microsoft.VisualStudio.TestTools.UnitTesting;
using
OpenQA.Selenium;
using
OpenQA.Selenium.Chrome;
using
OpenQA.Selenium.Firefox;
using OpenQA.Selenium.IE;
using
OpenQA.Selenium.Remote;
using
System;
[TestClass]
public class UnitTest1
{
private string baseURL;
private RemoteWebDriver driver;
private string browser;
public TestContext TestContext { get
;
set; }
[TestMethod]
[TestCategory("UI")]
public void AddPerson()
{
driver.Manage().Window.Maximize();
driver.Manage().Timeouts().ImplicitlyWait(
TimeSpan.FromSeconds(30));
driver.Navigate().GoToUrl(this.baseURL);
driver.FindElementByLinkText("People").Click();
driver.FindElementByLinkText("Create New").Click();
driver.FindElementById("FirstName").Clear();
driver.FindElementById("FirstName").SendKeys(browser);
driver.FindElementById("LastName").Clear();
driver.FindElementById("LastName").SendKeys("User");
driver.FindElementByCssSelector("input.btn").Click();
// Force
chrome to slow down so the click is registered
var
wait =
new OpenQA.Selenium.Support.UI.WebDriverWait(driver, TimeSpan.FromSeconds(30));
wait.Until(driver1 => ((IJavaScriptExecutor)driver).ExecuteScript("return document.readyState").Equals("complete"));
}
/// <summary>
/// Use TestCleanup to run code after each test has run
/// </summary>
[TestCleanup()]
public void MyTestCleanup()
{
driver.Quit();
}
[TestInitialize()]
public void MyTestInitialize()
{
browser = this.TestContext.Properties["browser"] != null
?
this.TestContext.Properties["browser"].ToString() : "ie";
switch
(browser)
{
case
"firefox":
driver = new FirefoxDriver();
break;
case
"chrome":
driver = new ChromeDriver();
break;
default:
driver = new InternetExplorerDriver();
break;
}
// To
generate code for this test, select "Generate Code for Coded UI Test"
from the shortcut menu and select one of the menu items.
if (this.TestContext.Properties["webAppUrl"
]
!=
null)
{
this
.baseURL
=
this.TestContext.Properties["webAppUrl"].ToString();
}
else
{
this
.baseURL
=
"http://{yourDockerHostName}.cloudapp.azure.com/";
}
}
}
}
Replace {yourDockerHostName}
with your Docker host name.
25.
Install Chrome on your VM
26.
Install Firefox on your VM
27.
Update your build to only run Unit Test during
your build
a.
Edit your build in VSTS
b.
Click the Visual Studio Test task
c.
Change the Test Filter criteria to
TestCategory=Unit
Making this change to your build will make sure it does not
try to run your UI test during your build. We only want to run the UI tests
during our release which we will configure in a future post.
28.
Add TestCategory attribute to
PeopleTracker.NetServices.Tests
a.
Open HomeControllerTest.cs
b.
Replace the contents of the file with the code
below
namespace
PeopleTracker.NetService.Tests.Controllers
{
using
Microsoft.VisualStudio.TestTools.UnitTesting;
using
PeopleTracker.NetService.Controllers;
using
System.Web.Mvc;
[TestClass]
public class HomeControllerTest
{
[TestMethod]
[TestCategory("Unit")]
public void Index()
{
// Arrange
HomeController
controller =
new HomeController();
// Act
ViewResult
result = controller.Index()
as ViewResult;
// Assert
Assert.IsNotNull(result);
Assert.AreEqual("Home Page", result.ViewBag.Title);
}
}
}
29.
Commit and push your changes
a.
From the View menu select Team Explorer
b.
Click Changes button
c.
Enter a comment
d.
Select Commit and Push
Committing your changes will trigger the build we configured
in a previous step. Verify that the build succeeds.