Extreme Coding: Take control of your code Exilesoft Johannes Brodwall Exilesoft Chief scientist @jhannes TODO: Preparator refactoring of MyTime
Feb 14, 2016
Extreme Coding:Take control of your code
ExilesoftJohannes Brodwall
Exilesoft Chief scientist
@jhannes
TODO:
Preparator refactoring of MyTime
Are you in control of the code?
Or is the code in control of you?
KataNew codeOld code
Part I
Practicing Test-Driven
Development
public class PrimeFactorsTest { @Test public void oneHasNoFactors() { assertTrue(getPrimeFactors(1).isEmpty()); }
} Think of the simplest test case
public class PrimeFactorsTest {
@Test public void oneHasNoFactors() { assertTrue(getPrimeFactors(1).isEmpty()); }
private List<Integer> getPrimeFactors(int i) { // TODO Auto-generated method stub return null; }
}
Make the code compile=> Test runs red
public class PrimeFactorsTest {
@Test public void oneHasNoFactors() { assertTrue(getPrimeFactors(1).isEmpty()); }
private List<Integer> getPrimeFactors(int i) { return new ArrayList<>(); }
}
The simplest thing to green
public class PrimeFactorsTest {
@Test public void oneHasNoFactors() { assertTrue(getPrimeFactors(1).isEmpty()); }
private List<Integer> getPrimeFactors(int i) { List<Integer> factors = new ArrayList<>(); return factors; }
}
Refactor
public class PrimeFactorsTest {
@Test public void oneHasNoFactors() { assertTrue(getPrimeFactors(1).isEmpty()); }
@Test public void factorsOfTwo() { assertEquals(Arrays.asList(2), getPrimeFactors(2)); }
private List<Integer> getPrimeFactors(int i) { List<Integer> factors = new ArrayList<>(); return factors; }
} The next simplest test=> Tests fail
public class PrimeFactorsTest {
@Test public void oneHasNoFactors() { assertTrue(getPrimeFactors(1).isEmpty()); }
@Test public void factorsOfTwo() { assertEquals(Arrays.asList(2), getPrimeFactors(2)); }
private List<Integer> getPrimeFactors(int i) { List<Integer> factors = new ArrayList<>(); if (i == 2) factors.add(2); return factors; }
} Simplest possible thing: Special case it
public class PrimeFactorsTest {
@Test public void oneHasNoFactors() { assertTrue(getPrimeFactors(1).isEmpty()); }
@Test public void factorsOfTwo() { assertEquals(Arrays.asList(2), getPrimeFactors(2)); }
private List<Integer> getPrimeFactors(int number) { List<Integer> factors = new ArrayList<>(); if (number == 2) factors.add(2); return factors; }
} Refactor – improve naming
public class PrimeFactorsTest {
// ... @Test public void factorsOfTwo() { assertEquals(Arrays.asList(2), getPrimeFactors(2)); }
@Test public void factorsOfThree() { assertEquals(Arrays.asList(3), getPrimeFactors(3)); }
private List<Integer> getPrimeFactors(int number) { List<Integer> factors = new ArrayList<>(); if (number == 2) factors.add(2); if (number == 3) factors.add(3); return factors; }} Simplest next case +
code
public class PrimeFactorsTest {
// ... @Test public void factorsOfTwo() { assertEquals(Arrays.asList(2), getPrimeFactors(2)); }
@Test public void factorsOfThree() { assertEquals(Arrays.asList(3), getPrimeFactors(3)); }
private List<Integer> getPrimeFactors(int number) { List<Integer> factors = new ArrayList<>(); if (number > 1) factors.add(number); return factors; }}
Refactor away duplication
public class PrimeFactorsTest {
// ... @Test public void factorsOfFour() { assertEquals(Arrays.asList(2,2), getPrimeFactors(4)); }
private List<Integer> getPrimeFactors(int number) { List<Integer> factors = new ArrayList<>(); if (number == 4) { factors.add(2); number /= 2; } if (number > 1) factors.add(number); return factors; }}
Next caseSpecial case result
public class PrimeFactorsTest {
// ... @Test public void factorsOfSix() { assertEquals(Arrays.asList(2,3), getPrimeFactors(6)); }
private List<Integer> getPrimeFactors(int number) { List<Integer> factors = new ArrayList<>(); if (number == 6) { factors.add(2); number /= 2; } if (number == 4) { factors.add(2); number /= 2; } if (number > 1) factors.add(number); return factors; }} Next case – duplicate
special case
public class PrimeFactorsTest {
// ... @Test public void factorsOfSix() { assertEquals(Arrays.asList(2,3), getPrimeFactors(6)); }
private List<Integer> getPrimeFactors(int number) { List<Integer> factors = new ArrayList<>(); int factor = 2; if (number % factor == 0) { factors.add(factor); number /= factor; } if (number > 1) factors.add(number); return factors; }} Refactor away
duplicationImportant design step!
public class PrimeFactorsTest {
// ... @Test public void factorsOfEight() { assertEquals(Arrays.asList(2,2,2), getPrimeFactors(8)); }
private List<Integer> getPrimeFactors(int number) { List<Integer> factors = new ArrayList<>(); int factor = 2; if (number % factor == 0) { factors.add(factor); number /= factor; } if (number > 1) factors.add(number); return factors; }} Next test – fails
public class PrimeFactorsTest {
// ... @Test public void factorsOfEight() { assertEquals(Arrays.asList(2,2,2), getPrimeFactors(8)); }
private List<Integer> getPrimeFactors(int number) { List<Integer> factors = new ArrayList<>(); int factor = 2; while (number % factor == 0) { factors.add(factor); number /= factor; } if (number > 1) factors.add(number); return factors; }} Simplest thing to make
it work!
public class PrimeFactorsTest {
// ... @Test public void factorsOfNine() { assertEquals(Arrays.asList(3,3), getPrimeFactors(9)); }
private List<Integer> getPrimeFactors(int number) { List<Integer> factors = new ArrayList<>(); for (int factor = 2; factor<number; factor++) { while (number % factor == 0) { factors.add(factor); number /= factor; } } if (number > 1) factors.add(number); return factors; }}
Next test and code
TDD – the steps
Think of next testsWrite a testMake it pass as simply as possibleRefactor: Add names, remove duplication
Think of next testsWrite a testMake it pass as simply as possibleRefactor: Add names, remove duplication
Think of next testsWrite a testMake it pass as simply as possibleRefactor: Add names, remove duplication
Think of next testsWrite a testMake it pass as simply as possibleRefactor: Add names, remove duplication
This is where the design comes:Refactor: Add names, remove duplication
You think too much!
You think too much!(about the wrong things)
Part II
Starting out with TDD
Problem domain:
Read measurement data
Given “some data” in the database
When a client sends a “Read register
Modbus message”
Then we respond with the correct
data
Shallow acceptance test
describe("Modbus data server", function() { it("should read saved data from correct register", function(done) { var measurements = [ sampleMeasurement({ cassette: 1, transponder: 1, thickness: 50.1 }), sampleMeasurement({ cassette: 1, transponder: 2, thickness: 50.2 }), ];
updateData(measurements, function() { readRegisters(cassette_start_pos(1), 4, function(err, data) { data.readFloatLE(0).should.be.approximately(50.1, 0.0001); data.readFloatLE(4).should.be.approximately(50.2, 0.0001); done(); }); }); });});
describe("Modbus data server", function() { it("should read saved data from correct register", function(done) { var measurements = [ sampleMeasurement({ cassette: 1, transponder: 1, thickness: 50.1 }), sampleMeasurement({ cassette: 1, transponder: 2, thickness: 50.2 }), ];
updateData(measurements, function() { readRegisters(cassette_start_pos(1), 4, function(err, data) { data.readFloatLE(0).should.be.approximately(50.1, 0.0001); data.readFloatLE(4).should.be.approximately(50.2, 0.0001); done(); }); }); });});
Given
describe("Modbus data server", function() { it("should read saved data from correct register", function(done) { var measurements = [ sampleMeasurement({ cassette: 1, transponder: 1, thickness: 50.1 }), sampleMeasurement({ cassette: 1, transponder: 2, thickness: 50.2 }), ];
updateData(measurements, function() { readRegisters(cassette_start_pos(1), 4, function(err, data) { data.readFloatLE(0).should.be.approximately(50.1, 0.0001); data.readFloatLE(4).should.be.approximately(50.2, 0.0001); done(); }); }); });});
When
describe("Modbus data server", function() { it("should read saved data from correct register", function(done) { var measurements = [ sampleMeasurement({ cassette: 1, transponder: 1, thickness: 50.1 }), sampleMeasurement({ cassette: 1, transponder: 2, thickness: 50.2 }), ];
updateData(measurements, function() { readRegisters(cassette_start_pos(1), 4, function(err, data) { data.readFloatLE(0).should.be.approximately(50.1, 0.0001); data.readFloatLE(4).should.be.approximately(50.2, 0.0001); done(); }); }); });});
Then
Shallow implementation
var registers = new buffer.Buffer(10000);var cassette_start_register = function(cassette_id) { return 0;};var readRegisters = function(start_register, register_count, callback) { var message = registers.slice(start_register*2, start_register*2 + register_count*2); callback(null, message);};var updateData = function(objects, callback) { for (var i=0; i<objects.length; i++) { var measurement = objects[i]; var transducer_pos = 2*cassette_start_register(measurement.cassette_id) + 4*(measurement.transponder_id - 1); registers.writeFloatLE(measurement.thickness, transducer_pos); } callback();};
Extending it to the database
var Sequelize = require("sequelize");var sequelize = new Sequelize('modbusfun', 'modbus', 'secret', { dialect: 'mysql', port: 3306, logging: false });
var Measurement = require('../lib/modbus-fun/measurement') .init(sequelize);var updateRegisters = function(callback) { Measurement.findMeasurements(function(err, objects) { for (var i=0; i<objects.length; i++) { var measurement = objects[i]; var transducer_pos = 2*cassette_start_register(measurement.cassette_id) + 4*(measurement.transponder_id - 1); registers.writeFloatLE(measurement.thickness, transducer_pos); } callback(); });};
Extending it to TCP
var myRegisters = Registers.createRegisters(Measurement);var server = myRegisters.createSocket();var modbusClient;
var readRegisters = function(start, count, callback) { modbusClient.readRegisters(start, count, function(err, data) { callback(err, data); });};
var readRegisters = function(offset, register_count, callback) { callbacks[++transactionId] = function(response_message) { callback(0, response_message.slice(10, 10 + register_count*2)); };
var fc = 0x04; var message = new buffer.Buffer(12); message.writeUInt16LE(transactionId, 0); message.writeUInt16LE(protocolId, 2); message.writeUInt16LE(6, 4); message.writeUInt8(unitId, 6); message.writeUInt8(fc, 7); message.writeUInt16LE(offset, 8); message.writeUInt16LE(register_count, 10);
client.write(message);};
Shallow – then deep
Part III
Getting in control of legacy code
public class HomeControllerTest{ [Test] public void GetEmployeeAvailabilityShouldXXX() { var homeController = new HomeController(); var result = homeController.GetEmployeeAvailability(); result.Should().BeNull(); }}
System.NullReferenceException : Object reference not set to an instance of an object. at Exilesoft.MyTime.Controllers.HomeController.GetEmployeeAvailability() in HomeController.cs: line 190 at HomeControllerTest.GetEmployeeAvailabilityShouldXXX() in HomeControllerTest.cs: line 11
public JsonResult GetEmployeeAvailability(){ string specialEventFullText = string.Empty; DateTime? selectedDate = (DateTime)Session[selectedDateKey]; if (selectedDate == null) selectedDate = DateTime.Now;
private DateTime SelectedDate{ get { if (Session == null) return DateTime.Now; return ((DateTime?) Session[selectedDateKey]) ?? DateTime.Now; }}
Ctrl-r, ctrl-m
Test goes reeeely slow and bombs:System.Data.SqlClient.SqlException : A network-related or instance-specific
error occurred while establishing a connection to SQL Server.
private string DisplaySpecialEvent(string specialEventFullText){ List<SpecialEvent> specilEventList = dbContext.SpecialEvents.Where(c => c.EventFromDate <= DateTime.Today && DateTime.Today <= c.EventToDate).ToList();
Ctrl-r, ctrl-m
private IEnumerable<SpecialEvent> SpecialEvents{ get { return dbContext.SpecialEvents; }}
public HomeController(){ dbContext = new Context(); SpecialEvents = dbContext.SpecialEvents;}
public HomeController(IEnumerable<SpecialEvent> specialEvents){ SpecialEvents = specialEvents;}
[Test]public void GetEmployeeAvailabilityShouldXXX(){ var homeController = new HomeController(new List<SpecialEvent>()); var result = homeController.GetEmployeeAvailability(); result.Should().BeNull();}
Test now runs fast, but encounters
EntityFramework issues
public JsonResult GetEmployeeAvailability(){ string specialEventFullText = string.Empty; var selectedDate = SelectedDate;
//SpecialEvent specilEvent = dbContext.SpecialEvents.Where(c => c.EventFromDate.Value.Year == selectedDate.Value.Year && // c.EventFromDate.Value.Month == selectedDate.Value.Month && c.EventFromDate.Value.Day == selectedDate.Value.Day).SingleOrDefault();
specialEventFullText = this.DisplaySpecialEvent(specialEventFullText);
var allAttendanceForDay = dbContext.Attendances.Where(a => a.Employee.IsEnable == true && a.Year == selectedDate.Year && a.Month == selectedDate.Month && a.Day == selectedDate.Day);
public JsonResult GetEmployeeAvailability(){ string specialEventFullText = string.Empty; var selectedDate = SelectedDate;
specialEventFullText = this.DisplaySpecialEvent(specialEventFullText);
var allAttendanceForDay = dbContext.Attendances.Where(a => a.Employee.IsEnable == true && a.Year == selectedDate.Year && a.Month == selectedDate.Month && a.Day == selectedDate.Day);
public JsonResult GetEmployeeAvailability(){ var selectedDate = SelectedDate; string specialEventFullText = DisplaySpecialEvent(string.Empty);
var allAttendanceForDay = dbContext.Attendances.Where(a => a.Employee.IsEnable == true && a.Year == selectedDate.Year && a.Month == selectedDate.Month && a.Day == selectedDate.Day);
Session[attendanceListKey] = result.ToList();
if (Session != null) Session[attendanceListKey] = result.ToList();
public List<int> GetExitLocationMachins(){ string _exitLocationMachines = ConfigurationManager.AppSettings["ExitLocationMachines"].ToString(); List<int> _locationIDList = new List<int>(); foreach (string locationID in _exitLocationMachines.Split(',')) _locationIDList.Add(int.Parse(locationID));
return _locationIDList;}
Really!?“Machins”?!
public List<int> GetExitLocationMachins(){ string _exitLocationMachines = ConfigurationManager.AppSettings["ExitLocationMachines"].ToString(); List<int> _locationIDList = new List<int>(); foreach (string locationID in _exitLocationMachines.Split(',')) _locationIDList.Add(int.Parse(locationID));
return _locationIDList;}
Ctrl-r, ctrl-r
public List<int> GetExitLocationMachines(){ string _exitLocationMachines = ConfigurationManager.AppSettings["ExitLocationMachines"].ToString(); List<int> _locationIDList = new List<int>(); foreach (string locationID in _exitLocationMachines.Split(',')) _locationIDList.Add(int.Parse(locationID));
return _locationIDList;}
[Test]public void GetEmployeeAvailabilityShouldXXX(){ ConfigurationManager.AppSettings["ExitLocationMachines"] = "200,300"; var homeController = new HomeController(new List<SpecialEvent>(), new List<Attendance>()); var result = homeController.GetEmployeeAvailability(); result.Should().BeNull();}
internal static List<EmployeeData> GetAllEmployees(){ //var tempValue = CasheEmployeeData(); string data = new WebClient().DownloadString(string.Format(ConfigurationManager.AppSettings["EmployeeWebApi"], "GetMyTimeEmployees")); JavaScriptSerializer jsonSerializer = new JavaScriptSerializer();
cacheEmployeeDataList = (List<EmployeeData>)jsonSerializer.Deserialize(data, typeof(List<EmployeeData>));
List<int> employeeEnrollmentsIds = EmployeeEnrollmentRepository.GetEmployeeEnrollments().Select(s=>s.EmployeeId).ToList();
return cacheEmployeeDataList.Where(s => employeeEnrollmentsIds.Contains(s.Id)).ToList();}
public IEnumerable<EmployeeData> EmployeeWebList { get; set; }
public HomeController(){ dbContext = new Context(); SpecialEvents = dbContext.SpecialEvents; Attendances = dbContext.Attendances; EmployeeWebList = new EmployeeWebGateway();}
public HomeController(IEnumerable<SpecialEvent> specialEvents, IEnumerable<Attendance> attendances, IEnumerable<EmployeeData> employeeWebList){ Attendances = attendances; SpecialEvents = specialEvents; EmployeeWebList = employeeWebList;}
public JsonResult GetEmployeeAvailability(){ ... int onsiteEmployeeCount = EmployeeRepository.EmployeesOnSite(selectedDate);
Ctrl-r, ctrl-i
int onsiteEmployeeCount = dbContext.EmployeesOnSite.Count(a => a.FromDate <= (DateTime?) selectedDate && a.ToDate >= (DateTime?) selectedDate);
Expected object to be <null>, but found System.Web.Mvc.JsonResult{ ContentEncoding = <null> ContentType = <null> Data = { GraphData = Time,Count, PeopleIn = 0, PeopleOut = 0, absentEmployeeCount = 0, totalEmployeeCount = 0, onSiteCount = 0, specilEvent = } JsonRequestBehavior = DenyGet MaxJsonLength = <null> RecursionLimit = <null>}.
Hurrah!
[Test]public void GetEmployeeAvailabilityShouldXXX(){ ConfigurationManager.AppSettings["ExitLocationMachines"] = "200,300"; var homeController = new HomeController( new List<SpecialEvent>(), new List<Attendance>(), new List<EmployeeData>(), new List<EmployeeOnSite>()); var result = homeController.GetEmployeeAvailability(); result.Should().BeNull();}
public HomeController(){ dbContext = new Context(); SpecialEvents = dbContext.SpecialEvents; EmployeesOnSite = dbContext.EmployeesOnSite; Attendances = dbContext.Attendances; EmployeeWebList = new EmployeeWebGateway();}
public HomeController(IEnumerable<SpecialEvent> specialEvents, IEnumerable<Attendance> attendances, IEnumerable<EmployeeData> employeeWebList, IEnumerable<EmployeeOnSite> employeesOnSite){ EmployeesOnSite = employeesOnSite; Attendances = attendances; SpecialEvents = specialEvents; EmployeeWebList = employeeWebList;}
[Test]public void GetEmployeeAvailabilityShouldTrackArrivedEmployees(){ ConfigurationManager.AppSettings["ExitLocationMachines"] = "200,300";
var today = DateTime.Today.AddDays(-1); var employee1 = new EmployeeEnrollment { IsEnable = true, EnrollNo = 1 }; var employee2 = new EmployeeEnrollment { IsEnable = true, EnrollNo = 2 }; _attendances.Add(SampleAttendance(today.AddHours(12), employee1)); _attendances.Add(SampleAttendance(today.AddHours(13).AddMinutes(30), employee2));
var homeController = new HomeController( new List<SpecialEvent>(), _attendances, new List<EmployeeData>(), new List<EmployeeOnSite>()); homeController.SelectedDate = today.AddHours(19).AddMinutes(23);
var result = homeController.GetEmployeeAvailability(); string graphData = GetProperty(result.Data, "GraphData").ToString(); graphData.Should().Contain("Time,Count\n"); graphData.Should().Contain("11:00:00,0"); graphData.Should().Contain("13:00:00,1"); graphData.Should().Contain("14:00:00,2"); graphData.Should().Contain("19:23:00,2"); graphData.Should().NotContain("20:20:00,");}
Testing legacy code
New up a class and call a method
Replace dependencies with
Lists (!)
Get the test to run
Add logic to the test
[Test]public void GetEmployeeAvailabilityShouldReduceEmployeesWhenExit(){ _attendances.Add(SampleAttendance(_today.AddHours(12).AddMinutes(30), _employee1)); _attendances.Add(SampleAttendance(_today.AddHours(13).AddMinutes(20), _employee1, "out", 200)); _homeController.SelectedDate = _today.AddHours(19).AddMinutes(23);
var result = _homeController.GetEmployeeAvailability(); string graphData = GetProperty(result.Data, "GraphData").ToString(); graphData.Should().Contain("12:00:00,0"); graphData.Should().Contain("13:00:00,1"); graphData.Should().Contain("14:00:00,0");}
[Test]public void GetEmployeeAvailabilityShouldCountEmployeesInside(){ _attendances.Add(SampleAttendance(_today.AddHours(12).AddMinutes(30), _employee1)); _attendances.Add(SampleAttendance(_today.AddHours(13).AddMinutes(20), _employee1, "out", 200));
_homeController.SelectedDate = _today.AddHours(8).AddMinutes(31); GetProperty(_homeController.GetEmployeeAvailability().Data, "PeopleOut").Should().Be(0);
_homeController.SelectedDate = _today.AddHours(12).AddMinutes(31); GetProperty(_homeController.GetEmployeeAvailability().Data, "PeopleIn").Should().Be(1); GetProperty(_homeController.GetEmployeeAvailability().Data, "PeopleOut").Should().Be(0);
_homeController.SelectedDate = _today.AddHours(13).AddMinutes(21); GetProperty(_homeController.GetEmployeeAvailability().Data, "PeopleIn").Should().Be(0); GetProperty(_homeController.GetEmployeeAvailability().Data, "PeopleOut").Should().Be(1);}
But: It’s not about the tests
It’s about the refactorings
• Rename• Extract method• Introduce variable• Introduce parameters• Inline method
The Challenge
Do 100 refactorings per day
Do 100 refactorings per day
Print out a cheat sheet with resharper refactorings
Practice a coding kata
Create a test for a legacy class
Give me feedback:
How likely are you to recommend this session to a co-worker on a scale from 1 (lowest) to 10 (highest)?
Optional: What's the reason for your number?
http://JohannesBrodwall.comhttp://exilesoft.com
http://twitter.com/jhannes
Thank you
Give me feedback:
How likely are you to recommend this session to a co-worker on a scale from 1 (lowest) to 10 (highest)?
Optional: What's the reason for your number?