Building DateTime for Arduino

Testing. Yup, testing. With this class it can’t be avoided. Actually, even better, instead of seeing testing as a chore, let’s use it to our advantage. We’ll build a suite of tests for the unfinished DateTime class and test it until it works.
The goal here is to write all the tests first and run them. Some percentage of them will fail. Then we modify the DateTime class until all tests pass. As we find issues along the way, we add more tests.
Get code here: https://github.com/prawnhead/ArduinoDateTime
For anyone not familiar with software testing (and I’m learning too) here’s a quick overview. Imagine we’re writing a class (Arduino “library”) such as DateTime. How do we know it works correctly? Well, we could create a DateTime for a particular date and time and check that the year, month, day, etcetera are all the same numbers that we used when we created the DateTime. What if we add 1 second to the date time, is it now the correct year, month, day and so on? What if the date and time we started with was 1 second to midnight and we added a second? Did the day roll over to the next date? It’s quite easy to write functions to test such things. The important thing is knowing which tests are required.
We start by acknowledging there is an infinite number of tests that could be written and run. It’s practically impossible to test all possible scenarios. If we’re going to be smart about this we’ll break up the tests into equivalence classes. Adding one second to a DateTime to see that the seconds are incremented is the same as if we added two seconds. Checking the minutes roll over by adding one second is the same as adding two seconds.
Example test:
  1. Create a DateTime from a set of individual values for year, month, day and so on. Check the DateTime.year(), DateTime.month(), etcetera are each equal to the numbers used to create the DateTime.
Below is the code I used to create the above test. This is the main sketch in the Arduino DateTime library at the moment (commit: 0549bb747cb7a76109cd090ffc25fefc8d10a9ae).

#include "DateTime.h" int tests = 0; int fails = 0; void setup() {   Serial.begin(115200);   Serial.println("setup()");   DateTime test = DateTime(2000, 1, 1, 1, 1, 1, 0);   testDateTime(test, 2000, 1, 1, 1, 1, 1, 0);   printTestResults(); } void loop() {   delay(1000); } void testDateTime(DateTime test, int year, int month, int day, int hour, int minute, int second, int millisecond) {   boolean fail = false;   if (!compare(year, test.year(), "Year")) fail = true;   if (!compare(month, test.month(), "Month")) fail = true;   if (!compare(day, test.day(), "Day")) fail = true;   if (!compare(hour, test.hour(), "Hour")) fail = true;   if (!compare(minute, test.minute(), "Minute")) fail = true;   if (!compare(second, test.second(), "Second")) fail = true;   if (!compare(millisecond, test.millisecond(), "Millisecond")) fail = true;   if (fail) fails++;   tests++;   Serial.println(test.toString()); } boolean compare(int expected, int actual, String description) {   if (expected == actual) return true;   Serial.print("Compare failed on ");   Serial.print(description);   Serial.print(". Expected: ");   Serial.print(expected);   Serial.print(". Actual: ");   Serial.println(actual);   return false; } void printTestResults() {   Serial.print(tests);   Serial.print(" test completed. ");   Serial.print(fails);   Serial.print(" failures. ");   Serial.print((tests - fails)/tests * 100);   Serial.println("% pass rate.");   if (fails) Serial.println("\nFAIL!"); }

The output from running this is:

setup()
01/01/2000 01:01:01
1 test completed. 0 failures. 100% pass rate.


Importantly, if in setup() I change one value in the testDateTime() line, the test fails. So when run with the following setup() function…

void setup() {   Serial.begin(115200);   Serial.println("setup()");   DateTime test = DateTime(2000, 1, 1, 1, 1, 1, 0);   testDateTime(test, 2000, 1, 1, 1, 1, 1, 7);   printTestResults(); }

… the test fails as seen below in this output.

setup()
Compare failed on Millisecond. Expected: 7. Actual: 0
01/01/2000 01:01:01
1 test completed. 1 failures. 0% pass rate.


FAIL!

With this framework in place, I can create DateTime objects, manipulate them, then test they are what they should be. Here’s a far more interesting test. In this test we create a DateTime, add a millisecond to it and check it.

void setup() {   Serial.begin(115200);   Serial.println("setup()");   DateTime test = DateTime(2000, 1, 1, 1, 1, 1, 0);   testDateTime(test, 2000, 1, 1, 1, 1, 1, 0);   test.add(1, DateTime::Millisecond);   testDateTime(test, 2000, 1, 1, 1, 1, 1, 1);   printTestResults(); }

These two tests pass. And so on. Now we need to ensure we test the functionality. We need tests to:
  • ensure values are correct for any DateTime created
  • tell if a DateTime is valid (no function for this yet)
  • add/subtract intervals from a DateTime and check correct
  • check milliseconds roll over to, and back from, seconds
  • check seconds roll over to, and back from, minutes
  • check minutes roll over to, and back from, hours
  • check hours roll over to, and back from, days
  • check days roll over to, and back from, months
  • check months roll over to, and back from, years
  • check months roll over to, and back from, the next month with the correct days in the month for each month including testing February within and without a leap year
  • check the days in each month
  • check the days in February for a normal year, leap year, century non-leap-year, four-century leap-year.
  • compare two dates and correctly determine which is less or if they are equal
  • convert to local time
  • daylight savings fall forward/back
  • correctly determine the day of the week from the date
  • return strings of the DateTime, month and day of week.
I have worked my way through the list until I have tests for creation of any arbitrary DateTime, addition of intervals, verification of rollovers between units, checks on length of each month and tests on leap years. I found a problem with the calculation of the month when adding/subtracting intervals. When the calculation of the month resulted in month = 0, I was altering this to month = 12 without a 'carry in' from years to make up for the change. I've fixed that now. Testing huh? I just got value out of all that work. That bug might have plagued me for years!

But I've hit a snag. My code so far is storing strings just where ever, which means the strings are in RAM and using up quite a bit of precious memory. I need to move my strings ("Monday", etc.) into program memory, probably using PROGMEM. But this is proving not to be simple - especially when I try to make my PROGMEM arrays part of a class. I need to go on and fix this issue separately.

Later I need to come back and change the design of the DateTime class to use byte date types where possible. This will minimise the footprint of each DateTime object on memory. At the moment it's just using int for everything.

That is all.

Comments