Skip to content

Commit

Permalink
Add unit tests for numerical habits
Browse files Browse the repository at this point in the history
  • Loading branch information
KristianTashkov committed Sep 26, 2021
1 parent 07e55f1 commit 9d8de8a
Show file tree
Hide file tree
Showing 2 changed files with 235 additions and 24 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,19 @@ class HabitFixtures(private val modelFactory: ModelFactory, private val habitLis
return habit
}

fun createEmptyNumericalHabit(targetType: NumericalHabitType): Habit {
val habit = modelFactory.buildHabit()
habit.type = HabitType.NUMERICAL
habit.name = "Run"
habit.question = "How many miles did you run today?"
habit.unit = "miles"
habit.targetType = targetType
habit.targetValue = 2.0
habit.color = PaletteColor(1)
saveIfSQLite(habit)
return habit
}

fun createLongHabit(): Habit {
val habit = createEmptyHabit()
habit.frequency = Frequency(3, 7)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,10 @@ import org.junit.Before
import org.junit.Test
import java.util.ArrayList

class ScoreListTest : BaseUnitTest() {
private lateinit var habit: Habit
private lateinit var today: Timestamp
open class BaseScoreListTest : BaseUnitTest() {
protected lateinit var habit: Habit
protected lateinit var today: Timestamp

@Before
@Throws(Exception::class)
override fun setUp() {
Expand All @@ -39,6 +40,28 @@ class ScoreListTest : BaseUnitTest() {
habit = fixtures.createEmptyHabit()
}

protected fun checkScoreValues(expectedValues: DoubleArray) {
var current = today
val scores = habit.scores
for (expectedValue in expectedValues) {
assertThat(scores[current].value, IsCloseTo.closeTo(expectedValue, E))
current = current.minus(1)
}
}

companion object {
const val E = 1e-6
}
}

class YesNoScoreListTest : BaseScoreListTest() {
@Before
@Throws(Exception::class)
override fun setUp() {
super.setUp()
habit = fixtures.createEmptyHabit()
}

@Test
fun test_getValue() {
check(0, 20)
Expand Down Expand Up @@ -122,18 +145,6 @@ class ScoreListTest : BaseUnitTest() {
checkScoreValues(expectedValues)
}

@Test
fun test_withZeroTarget() {
habit = fixtures.createNumericalHabit()
habit.targetValue = 0.0
habit.recompute()
assertTrue(habit.scores[today].value.isFinite())

habit.targetType = NumericalHabitType.AT_MOST
habit.recompute()
assertTrue(habit.scores[today].value.isFinite())
}

@Test
fun test_imperfectNonDaily() {
// If the habit should be performed 3 times per week and the user misses 1 repetition
Expand Down Expand Up @@ -259,17 +270,204 @@ class ScoreListTest : BaseUnitTest() {
val entries = habit.originalEntries
entries.add(Entry(today.minus(day), Entry.SKIP))
}
}

private fun checkScoreValues(expectedValues: DoubleArray) {
var current = today
val scores = habit.scores
for (expectedValue in expectedValues) {
assertThat(scores[current].value, IsCloseTo.closeTo(expectedValue, E))
current = current.minus(1)
}
open class NumericalScoreListTest : BaseScoreListTest() {
protected fun addEntry(day: Int, value: Int) {
val entries = habit.originalEntries
entries.add(Entry(today.minus(day), value))
}

companion object {
private const val E = 1e-6
protected fun addEntries(from: Int, to: Int, value: Int) {
val entries = habit.originalEntries
for (i in from until to) entries.add(Entry(today.minus(i), value))
habit.recompute()
}
}

class NumericalAtLeastScoreListTest : NumericalScoreListTest() {
@Before
@Throws(Exception::class)
override fun setUp() {
super.setUp()
habit = fixtures.createEmptyNumericalHabit(NumericalHabitType.AT_LEAST)
}

@Test
fun test_withZeroTarget() {
habit = fixtures.createNumericalHabit()
habit.targetValue = 0.0
habit.recompute()
assertTrue(habit.scores[today].value.isFinite())
}

@Test
fun test_getValue() {
addEntries(0, 20, 2000)
val expectedValues = doubleArrayOf(
0.655747,
0.636894,
0.617008,
0.596033,
0.573910,
0.550574,
0.525961,
0.500000,
0.472617,
0.443734,
0.413270,
0.381137,
0.347244,
0.311495,
0.273788,
0.234017,
0.192067,
0.147820,
0.101149,
0.051922,
0.000000,
0.000000,
0.000000
)
checkScoreValues(expectedValues)
}

@Test
fun test_recompute() {
assertThat(habit.scores[today].value, IsCloseTo.closeTo(0.0, E))
addEntries(0, 2, 2000)
assertThat(habit.scores[today].value, IsCloseTo.closeTo(0.101149, E))
habit.frequency = Frequency(1, 2)
habit.recompute()
assertThat(habit.scores[today].value, IsCloseTo.closeTo(0.072631, E))
}

@Test
fun shouldAchieveHighScoreInReasonableTime() {
// Daily habits should achieve at least 99% in 3 months
habit = fixtures.createEmptyNumericalHabit(NumericalHabitType.AT_LEAST)
habit.frequency = Frequency.DAILY
for (i in 0..89) addEntry(i, 2000)
habit.recompute()
assertThat(habit.scores[today].value, OrderingComparison.greaterThan(0.99))

// Weekly habits should achieve at least 99% in 9 months
habit = fixtures.createEmptyNumericalHabit(NumericalHabitType.AT_LEAST)
habit.frequency = Frequency.WEEKLY
for (i in 0..38) addEntry(7 * i, 2000)
habit.recompute()
assertThat(habit.scores[today].value, OrderingComparison.greaterThan(0.99))

// Monthly habits should achieve at least 99% in 18 months
habit.frequency = Frequency(1, 30)
for (i in 0..17) addEntry(30 * i, 2000)
habit.recompute()
assertThat(habit.scores[today].value, OrderingComparison.greaterThan(0.99))
}

@Test
fun shouldAchieveComparableScoreToProgress() {
addEntries(0, 500, 1000)
assertThat(habit.scores[today].value, IsCloseTo.closeTo(0.5, E))

addEntries(0, 500, 500)
assertThat(habit.scores[today].value, IsCloseTo.closeTo(0.25, E))
}

@Test
fun overeachievingIsntRelevant() {
addEntry(0, 10000000)
habit.recompute()
assertThat(habit.scores[today].value, IsCloseTo.closeTo(0.051922, E))
}
}

class NumericalAtMostScoreListTest : NumericalScoreListTest() {
@Before
@Throws(Exception::class)
override fun setUp() {
super.setUp()
habit = fixtures.createEmptyNumericalHabit(NumericalHabitType.AT_MOST)
}

@Test
fun test_withZeroTarget() {
habit = fixtures.createNumericalHabit()
habit.targetType = NumericalHabitType.AT_MOST
habit.targetValue = 0.0
habit.recompute()
assertTrue(habit.scores[today].value.isFinite())
}

@Test
fun test_getValue() {
addEntry(20, 1000)
addEntries(0, 20, 5000)
val expectedValues = doubleArrayOf(
0.344253,
0.363106,
0.382992,
0.403967,
0.426090,
0.449426,
0.474039,
0.500000,
0.527383,
0.556266,
0.586730,
0.618863,
0.652756,
0.688505,
0.726212,
0.765983,
0.807933,
0.852180,
0.898851,
0.948078,
1.0,
0.0,
0.0
)
checkScoreValues(expectedValues)
}

@Test
fun test_recompute() {
habit.recompute()
assertThat(habit.scores[today].value, IsCloseTo.closeTo(1.0, E))
addEntries(0, 2, 5000)
assertThat(habit.scores[today].value, IsCloseTo.closeTo(0.898850, E))
habit.frequency = Frequency(1, 2)
habit.recompute()
assertThat(habit.scores[today].value, IsCloseTo.closeTo(0.927369, E))
}

@Test
fun shouldAchieveComparableScoreToProgress() {
addEntries(0, 500, 3000)
assertThat(habit.scores[today].value, IsCloseTo.closeTo(0.5, E))

addEntries(0, 500, 3500)
assertThat(habit.scores[today].value, IsCloseTo.closeTo(0.25, E))
}

@Test
fun undereachievingIsntRelevant() {
addEntry(1, 10000000)
habit.recompute()
assertThat(habit.scores[today].value, IsCloseTo.closeTo(0.950773, E))
}

@Test
fun overeachievingIsntRelevant() {
addEntry(0, 5000)

addEntry(1, 0)
habit.recompute()
assertThat(habit.scores[today].value, IsCloseTo.closeTo(0.948077, E))

addEntry(1, 1000)
habit.recompute()
assertThat(habit.scores[today].value, IsCloseTo.closeTo(0.948077, E))
}
}

0 comments on commit 9d8de8a

Please sign in to comment.