From 979ff9e765e3c08501cbd00354a87013853fe796 Mon Sep 17 00:00:00 2001 From: vlad Date: Tue, 19 Jul 2016 15:01:30 -0400 Subject: Unit tests for core code and bug fixes --- src/test/scala/com/drivergrp/core/CoreTest.scala | 30 +++ .../scala/com/drivergrp/core/GeneratorsTest.scala | 206 +++++++++++++++++++++ .../scala/com/drivergrp/core/MessagesTest.scala | 80 ++++++++ src/test/scala/com/drivergrp/core/RestTest.scala | 40 ++++ src/test/scala/com/drivergrp/core/StatsTest.scala | 43 +++++ src/test/scala/com/drivergrp/core/TimeTest.scala | 57 ++++++ 6 files changed, 456 insertions(+) create mode 100644 src/test/scala/com/drivergrp/core/CoreTest.scala create mode 100644 src/test/scala/com/drivergrp/core/GeneratorsTest.scala create mode 100644 src/test/scala/com/drivergrp/core/MessagesTest.scala create mode 100644 src/test/scala/com/drivergrp/core/RestTest.scala create mode 100644 src/test/scala/com/drivergrp/core/StatsTest.scala create mode 100644 src/test/scala/com/drivergrp/core/TimeTest.scala (limited to 'src/test/scala') diff --git a/src/test/scala/com/drivergrp/core/CoreTest.scala b/src/test/scala/com/drivergrp/core/CoreTest.scala new file mode 100644 index 0000000..005cda5 --- /dev/null +++ b/src/test/scala/com/drivergrp/core/CoreTest.scala @@ -0,0 +1,30 @@ +package com.drivergrp.core + +import java.io.ByteArrayOutputStream + +import org.scalatest.mock.MockitoSugar +import org.scalatest.{FlatSpec, Matchers} +import org.mockito.Mockito._ + +class CoreTest extends FlatSpec with Matchers with MockitoSugar { + + "'make' function" should "allow initialization for objects" in { + + val createdAndInitializedValue = make(new ByteArrayOutputStream(128)) { baos => + baos.write(Array(1.toByte, 1.toByte, 0.toByte)) + } + + createdAndInitializedValue.toByteArray should be(Array(1.toByte, 1.toByte, 0.toByte)) + } + + "'using' function" should "call close after performing action on resource" in { + + val baos = mock[ByteArrayOutputStream] + + using(baos /* usually new ByteArrayOutputStream(128) */ ) { baos => + baos.write(Array(1.toByte, 1.toByte, 0.toByte)) + } + + verify(baos).close() + } +} diff --git a/src/test/scala/com/drivergrp/core/GeneratorsTest.scala b/src/test/scala/com/drivergrp/core/GeneratorsTest.scala new file mode 100644 index 0000000..9332e7f --- /dev/null +++ b/src/test/scala/com/drivergrp/core/GeneratorsTest.scala @@ -0,0 +1,206 @@ +package com.drivergrp.core + +import org.scalatest.{Assertions, FlatSpec, Matchers} + +class GeneratorsTest extends FlatSpec with Matchers with Assertions { + import generators._ + + "Generators" should "be able to generate com.drivergrp.core.Id identifiers" in { + + val generatedId1 = nextId[String]() + val generatedId2 = nextId[String]() + val generatedId3 = nextId[Long]() + + generatedId1 should be >= 0L + generatedId2 should be >= 0L + generatedId3 should be >= 0L + generatedId1 should not be generatedId2 + generatedId2 should !==(generatedId3) + } + + it should "be able to generate com.drivergrp.core.Name names" in { + + nextName[String]() should not be nextName[String]() + nextName[String]().length should be >= 0 + + val fixedLengthName = nextName[String](10) + fixedLengthName.length should be <= 10 + assert(!fixedLengthName.exists(_.isControl)) + } + + it should "be able to generate strings" in { + + nextString() should not be nextString() + nextString().length should be >= 0 + + val fixedLengthString = nextString(20) + fixedLengthString.length should be <= 20 + assert(!fixedLengthString.exists(_.isControl)) + } + + it should "be able to generate options which are sometimes have values and sometimes not" in { + + val generatedOption = nextOption("2") + + generatedOption should not contain "1" + assert(generatedOption === Some("2") || generatedOption === None) + } + + it should "be able to generate a pair of two generated values" in { + + val constantPair = nextPair("foo", 1L) + constantPair._1 should be("foo") + constantPair._2 should be(1L) + + val generatedPair = nextPair(nextId[Int](), nextName[Int]()) + + generatedPair._1 should be > 0L + generatedPair._2.length should be > 0 + + nextPair(nextId[Int](), nextName[Int]()) should not be + nextPair(nextId[Int](), nextName[Int]()) + } + + it should "be able to generate a triad of two generated values" in { + + val constantTriad = nextTriad("foo", "bar", 1L) + constantTriad._1 should be("foo") + constantTriad._2 should be("bar") + constantTriad._3 should be(1L) + + val generatedTriad = nextTriad(nextId[Int](), nextName[Int](), nextBigDecimal()) + + generatedTriad._1 should be > 0L + generatedTriad._2.length should be > 0 + generatedTriad._3 should be >= BigDecimal(0.00) + + nextTriad(nextId[Int](), nextName[Int](), nextBigDecimal()) should not be + nextTriad(nextId[Int](), nextName[Int](), nextBigDecimal()) + } + + it should "be able to generate a time value" in { + + val generatedTime = nextTime() + val currentTime = System.currentTimeMillis() + + generatedTime.millis should be >= 0L + generatedTime.millis should be <= currentTime + } + + it should "be able to generate a time range value" in { + + val generatedTimeRange = nextTimeRange() + val currentTime = System.currentTimeMillis() + + generatedTimeRange.start.millis should be >= 0L + generatedTimeRange.start.millis should be <= currentTime + generatedTimeRange.end.millis should be >= 0L + generatedTimeRange.end.millis should be <= currentTime + generatedTimeRange.start.millis should be <= generatedTimeRange.end.millis + } + + it should "be able to generate a BigDecimal value" in { + + val defaultGeneratedBigDecimal = nextBigDecimal() + + defaultGeneratedBigDecimal should be >= BigDecimal(0.00) + defaultGeneratedBigDecimal should be <= BigDecimal(1000000.00) + defaultGeneratedBigDecimal.precision should be(2) + + val unitIntervalBigDecimal = nextBigDecimal(1.00, 8) + + unitIntervalBigDecimal should be >= BigDecimal(0.00) + unitIntervalBigDecimal should be <= BigDecimal(1.00) + unitIntervalBigDecimal.precision should be(8) + } + + it should "be able to generate a specific value from a set of values" in { + + val possibleOptions = Set(1, 3, 5, 123, 0, 9) + + val pick1 = generators.oneOf(possibleOptions) + val pick2 = generators.oneOf(possibleOptions) + val pick3 = generators.oneOf(possibleOptions) + + possibleOptions should contain(pick1) + possibleOptions should contain(pick2) + possibleOptions should contain(pick3) + + val pick4 = generators.oneOf(1, 3, 5, 123, 0, 9) + val pick5 = generators.oneOf(1, 3, 5, 123, 0, 9) + val pick6 = generators.oneOf(1, 3, 5, 123, 0, 9) + + possibleOptions should contain(pick4) + possibleOptions should contain(pick5) + possibleOptions should contain(pick6) + + Set(pick1, pick2, pick3, pick4, pick5, pick6).size should be >= 1 + } + + it should "be able to generate array with values generated by generators" in { + + val arrayOfTimes = arrayOf(nextTime(), 16) + arrayOfTimes.length should be <= 16 + + val arrayOfBigDecimals = arrayOf(nextBigDecimal(), 8) + arrayOfBigDecimals.length should be <= 8 + } + + it should "be able to generate seq with values generated by generators" in { + + val seqOfTimes = seqOf(nextTime(), 16) + seqOfTimes.size should be <= 16 + + val seqOfBigDecimals = seqOf(nextBigDecimal(), 8) + seqOfBigDecimals.size should be <= 8 + } + + it should "be able to generate vector with values generated by generators" in { + + val vectorOfTimes = vectorOf(nextTime(), 16) + vectorOfTimes.size should be <= 16 + + val vectorOfStrings = seqOf(nextString(), 8) + vectorOfStrings.size should be <= 8 + } + + it should "be able to generate list with values generated by generators" in { + + val listOfTimes = listOf(nextTime(), 16) + listOfTimes.size should be <= 16 + + val listOfBigDecimals = seqOf(nextBigDecimal(), 8) + listOfBigDecimals.size should be <= 8 + } + + it should "be able to generate set with values generated by generators" in { + + val setOfTimes = vectorOf(nextTime(), 16) + setOfTimes.size should be <= 16 + + val setOfBigDecimals = seqOf(nextBigDecimal(), 8) + setOfBigDecimals.size should be <= 8 + } + + it should "be able to generate maps with keys and values generated by generators" in { + + val generatedConstantMap = mapOf(10, "key", 123) + generatedConstantMap.size should be <= 1 + assert(generatedConstantMap.keys.forall(_ == "key")) + assert(generatedConstantMap.values.forall(_ == 123)) + + val generatedMap = mapOf(10, nextString(10), nextBigDecimal()) + assert(generatedMap.keys.forall(_.length <= 10)) + assert(generatedMap.values.forall(_ >= BigDecimal(0.00))) + } + + it should "compose deeply" in { + + val generatedNestedMap = mapOf(10, nextString(10), nextPair(nextBigDecimal(), nextOption(123))) + + generatedNestedMap.size should be <= 10 + generatedNestedMap.keySet.size should be <= 10 + generatedNestedMap.values.size should be <= 10 + assert(generatedNestedMap.values.forall(value => !value._2.exists(_ != 123))) + } +} diff --git a/src/test/scala/com/drivergrp/core/MessagesTest.scala b/src/test/scala/com/drivergrp/core/MessagesTest.scala new file mode 100644 index 0000000..21fe30a --- /dev/null +++ b/src/test/scala/com/drivergrp/core/MessagesTest.scala @@ -0,0 +1,80 @@ +package com.drivergrp.core + +import java.util.Locale + +import com.drivergrp.core.logging.Logger +import com.drivergrp.core.messages.Messages +import com.typesafe.config.{ConfigException, ConfigFactory} +import org.mockito.Mockito._ +import org.scalatest.mock.MockitoSugar +import org.scalatest.{FlatSpec, Matchers} + +import scala.collection.JavaConversions._ + +class MessagesTest extends FlatSpec with Matchers with MockitoSugar { + + val englishLocaleMessages = + Map("en.greeting" -> "Hello {0}!", "en.greetingFullName" -> "Hello {0} {1} {2}!", "en.hello" -> "Hello world!") + + "Messages" should "read messages from config and format with parameters" in { + + val log = mock[Logger] + val messagesConfig = ConfigFactory.parseMap(englishLocaleMessages) + + val messages = Messages.messages(messagesConfig, log, Locale.US) + + messages("hello") should be("Hello world!") + messages("greeting", "Homer") should be("Hello Homer!") + messages("greetingFullName", "Homer", "J", "Simpson") should be("Hello Homer J Simpson!") + } + + it should "be able to read messages for different locales" in { + + val log = mock[Logger] + + val messagesConfig = ConfigFactory.parseMap( + englishLocaleMessages ++ Map( + "zh.hello" -> "你好,世界!", + "zh.greeting" -> "你好,{0}!", + "zh.greetingFullName" -> "你好,{0} {1} {2}!" + )) + + val englishMessages = Messages.messages(messagesConfig, log, Locale.US) + val englishMessagesToo = Messages.messages(messagesConfig, log, Locale.ENGLISH) + val chineseMessages = Messages.messages(messagesConfig, log, Locale.CHINESE) + + englishMessages("hello") should be("Hello world!") + englishMessages("greeting", "Homer") should be("Hello Homer!") + englishMessages("greetingFullName", "Homer", "J", "Simpson") should be("Hello Homer J Simpson!") + + englishMessagesToo("hello") should be(englishMessages("hello")) + englishMessagesToo("greeting", "Homer") should be(englishMessages("greeting", "Homer")) + englishMessagesToo("greetingFullName", "Homer", "J", "Simpson") should be( + englishMessages("greetingFullName", "Homer", "J", "Simpson")) + + chineseMessages("hello") should be("你好,世界!") + chineseMessages("greeting", "Homer") should be("你好,Homer!") + chineseMessages("greetingFullName", "Homer", "J", "Simpson") should be("你好,Homer J Simpson!") + } + + it should "raise exception when locale is not available" in { + + val log = mock[Logger] + val messagesConfig = ConfigFactory.parseMap(englishLocaleMessages) + + an[ConfigException.Missing] should be thrownBy + Messages.messages(messagesConfig, log, Locale.GERMAN) + } + + it should "log a problem, when there is no message for key" in { + + val log = mock[Logger] + val messagesConfig = ConfigFactory.parseMap(englishLocaleMessages) + + val messages = Messages.messages(messagesConfig, log, Locale.US) + + messages("howdy") should be("howdy") + + verify(log).error(s"Message with key 'howdy' not found for locale 'en'") + } +} diff --git a/src/test/scala/com/drivergrp/core/RestTest.scala b/src/test/scala/com/drivergrp/core/RestTest.scala new file mode 100644 index 0000000..68be55c --- /dev/null +++ b/src/test/scala/com/drivergrp/core/RestTest.scala @@ -0,0 +1,40 @@ +package com.drivergrp.core + +import com.drivergrp.core.time.provider.SystemTimeProvider +import org.scalatest.{FlatSpec, Matchers} + +class RestTest extends FlatSpec with Matchers { + + "Json format for Id" should "read and write correct JSON" in { + + val referenceId = Id[String](1312L) + + val writtenJson = com.drivergrp.core.rest.basicFormats.idFormat.write(referenceId) + writtenJson.prettyPrint should be("1312") + + val parsedId = com.drivergrp.core.rest.basicFormats.idFormat.read(writtenJson) + parsedId should be(referenceId) + } + + "Json format for Name" should "read and write correct JSON" in { + + val referenceName = Name[String]("Homer") + + val writtenJson = com.drivergrp.core.rest.basicFormats.nameFormat.write(referenceName) + writtenJson.prettyPrint should be("\"Homer\"") + + val parsedName = com.drivergrp.core.rest.basicFormats.nameFormat.read(writtenJson) + parsedName should be(referenceName) + } + + "Json format for Time" should "read and write correct JSON" in { + + val referenceTime = new SystemTimeProvider().currentTime() + + val writtenJson = com.drivergrp.core.rest.basicFormats.timeFormat.write(referenceTime) + writtenJson.prettyPrint should be("{\n \"timestamp\": " + referenceTime.millis + "\n}") + + val parsedTime = com.drivergrp.core.rest.basicFormats.timeFormat.read(writtenJson) + parsedTime should be(referenceTime) + } +} diff --git a/src/test/scala/com/drivergrp/core/StatsTest.scala b/src/test/scala/com/drivergrp/core/StatsTest.scala new file mode 100644 index 0000000..c4f449b --- /dev/null +++ b/src/test/scala/com/drivergrp/core/StatsTest.scala @@ -0,0 +1,43 @@ +package com.drivergrp.core + +import com.drivergrp.core.logging.Logger +import com.drivergrp.core.stats.LogStats +import com.drivergrp.core.time.{Time, TimeRange} +import org.scalatest.mock.MockitoSugar +import org.scalatest.{FlatSpec, Matchers} +import org.mockito.Mockito._ + +class StatsTest extends FlatSpec with Matchers with MockitoSugar { + + "Stats" should "format and store all recorded data" in { + + val log = mock[Logger] + val stats = new LogStats(log) + + stats.recordStats(Seq(), TimeRange(Time(2L), Time(5L)), BigDecimal(123.324)) + verify(log).audit(s"(2-5)=123.324") + + stats.recordStats("stat", TimeRange(Time(5L), Time(5L)), BigDecimal(333L)) + verify(log).audit(s"stat(5-5)=333") + + stats.recordStats("stat", Time(934L), 123) + verify(log).audit(s"stat(934-934)=123") + + stats.recordStats("stat", Time(0L), 123) + verify(log).audit(s"stat(0-0)=123") + } + + it should "format BigDecimal with all precision digits" in { + + val log = mock[Logger] + val stats = new LogStats(log) + + stats.recordStats(Seq("root", "group", "stat", "substat"), + TimeRange(Time(1467381889834L), Time(1468937089834L)), + BigDecimal(3.333333333333333)) + verify(log).audit(s"root.group.stat.substat(1467381889834-1468937089834)=3.333333333333333") + + stats.recordStats("stat", Time(1233L), BigDecimal(0.00000000000000000000001)) + verify(log).audit(s"stat(1233-1233)=0.000000000000000000000010") + } +} diff --git a/src/test/scala/com/drivergrp/core/TimeTest.scala b/src/test/scala/com/drivergrp/core/TimeTest.scala new file mode 100644 index 0000000..ad390c8 --- /dev/null +++ b/src/test/scala/com/drivergrp/core/TimeTest.scala @@ -0,0 +1,57 @@ +package com.drivergrp.core + +import com.drivergrp.core.time.{Time, _} +import org.scalatest.{FlatSpec, Matchers} + +import scala.concurrent.duration._ + +class TimeTest extends FlatSpec with Matchers { + + "Time" should "have correct methods to compare" in { + + Time(234L).isAfter(Time(123L)) should be(true) + Time(123L).isAfter(Time(123L)) should be(false) + Time(123L).isAfter(Time(234L)) should be(false) + + Time(234L).isBefore(Time(123L)) should be(false) + Time(123L).isBefore(Time(123L)) should be(false) + Time(123L).isBefore(Time(234L)) should be(true) + } + + it should "not modify time" in { + + Time(234L).millis should be(234L) + } + + it should "support arithmetic with scala.concurrent.duration" in { + + Time(123L).advanceBy(0 minutes).millis should be(123L) + Time(123L).advanceBy(1 second).millis should be(123L + Second) + Time(123L).advanceBy(4 days).millis should be(123L + 4 * Days) + } + + it should "have ordering defined correctly" in { + + Seq(Time(321L), Time(123L), Time(231L)).sorted should + contain theSameElementsInOrderAs Seq(Time(123L), Time(231L), Time(321L)) + } + + it should "reset to the start of the period, e.g. month" in { + + startOfMonth(Time(1468937089834L)) should be(Time(1467381889834L)) + startOfMonth(Time(1467381889834L)) should be(Time(1467381889834L)) // idempotent + } + + it should "have correct textual representations" in { + + textualDate(Time(1468937089834L)) should be("July 19, 2016") + textualTime(Time(1468937089834L)) should be("Jul 19, 2016 10:04:49 AM") + } + + "TimeRange" should "have duration defined as a difference of start and end times" in { + + TimeRange(Time(321L), Time(432L)).duration should be(111.milliseconds) + TimeRange(Time(432L), Time(321L)).duration should be((-111).milliseconds) + TimeRange(Time(333L), Time(333L)).duration should be(0.milliseconds) + } +} -- cgit v1.2.3