Fast Feedback

Performant Tests

Why Worry About Fast Tests?

Why Worry About Fast Tests?

sonic contain

Slow Tests Are

  • Boring
  • Distracting
  • Frustrating
  • The Past

sonic contain

How Fast is Fast Enough?

  • Should be realtime
  • Should be constantly running (jest?)
  • Part of breaking the feedback loop down

sonic contain

Test Definitions

  • By Amount of Code
  • By Time Run
  • By Pieces Involved

sonic

The Testing Pyramid

contain

The Testing Pyramid

  • Are you creating the right amount of tests at the right level?
  • Can you push the test down the pyramid?

Speedbumps

  • Reflection (Spring!)
  • Reading from the DB
  • Reading from the File System
  • Internet Connections

sonic

Where Can I Cut Time?

  • What tests take the longest time to run?
    • Are there outliers?
  • Where are we doing more setup than needed?
  • Where can we recycle or share setup?
  • Where can we mock?
  • Where can test a smaller chunk?

sonic

Where Can I Cut Time?

contain

sonic

Dangers of the Knife

  • Shared State
  • Missed integrations

sonic

Example: Startup Reflection with Quest Command

private fun loadCommands(): List<Command> {
   return ReflectionTools.getAllCommands().asSequence().map { it.newInstance() }.toList()
}

Example: Startup Reflection with Quest Command

fun getAllCommands(): List<Class<out Command>> {
   return getClassesFromFile(commandsFile)
}

fun getAllEvents(): List<Class<out Event>> {
   return getClassesFromFile(eventsFile)
}


private fun <E> getClassesFromFile(file: String): List<Class<E>> {
   val classes = mutableListOf<Class<E>>()
   val content = this::class.java.getResource(file).readText()
   content.trim().lines().forEach {
       try {
           val kClass = Class.forName(it) as Class<E>
           classes.add(kClass)
       } catch (e: ClassNotFoundException) {
           println("Couldn't find class $it")
           throw e
       }
   }
   return classes.toList()
}

Problems with the old approach

  • Reflection at startup
  • Discovering a known property

sonic

Example: Startup Reflection with Quest Command

fun main() {
   ReflectionTools.generateFiles()
   println("Build complete")
}
fun generateFiles() {
   generateCollectionsFile(Command::class)
   generateCollectionsFile(SpellCommand::class)

   generateResourcesFile(AIResource::class, AIBase::class)

Example: Startup Reflection with Quest Command

private fun writeGeneratedFile(collectedClass: KClass<*>, typeSuffix: String, classes: String) {
   val packageName = collectedClass.java.packageName.replace(".", "/")
   File("./src/main/kotlin/$packageName/${collectedClass.simpleName}sGenerated.kt").printWriter().use {
       it.print(
           """
           package ${collectedClass.java.packageName}

           class ${collectedClass.simpleName}sGenerated : ${collectedClass.simpleName}sCollection {
               override val values: List<${collectedClass.qualifiedName}$typeSuffix> = listOf($classes)
           }
       """.trimIndent()
       )
   }
}

Example: Startup Reflection with Quest Command

class CommandsGenerated : CommandsCollection {
   override val values: List<core.commands.Command> = listOf(
      combat.attack.AttackCommand(), combat.block.BlockCommand(), 
      combat.dodge.DodgeCommand(), conversation.SpeakCommand(), core.commands.UnknownCommand(), 
      crafting.checkRecipe.RecipeCommand(), crafting.craft.CookCommand(), crafting.craft.CraftRecipeCommand(), 
      explore.examine.ExamineCommand(), explore.listen.ListenCommand(), explore.look.LookCommand(), 
      explore.map.ReadMapCommand(), explore.map.compass.ViewCompassCommand(), inventory.EquippedCommand(), 
      inventory.InventoryCommand(), inventory.dropItem.DropItemCommand(), inventory.equipItem.EquipItemCommand(), 
      inventory.equipItem.HoldItemCommand(), inventory.pickupItem.TakeItemCommand(), inventory.putItem.PutItemCommand(), 
      inventory.unEquipItem.UnEquipItemCommand(), magic.castSpell.CastCommand(), quests.journal.JournalCommand(), 
      status.rest.RestCommand(), status.status.StatusCommand(), system.ExitCommand(), system.RedoCommand(), 
      system.alias.AliasCommand(), system.debug.DebugCommand(), system.help.CommandsCommand(), system.help.HelpCommand(), 
      system.history.HistoryCommand(), system.persistance.changePlayer.PlayAsCommand(), system.persistance.loading.LoadCommand(), 
      system.persistance.newGame.CreateNewGameCommand(), system.persistance.saving.SaveCommand(), time.ViewTimeCommand(), 
      traveling.approach.ApproachCommand(), traveling.approach.RetreatCommand(), traveling.climb.ClimbCommand(), 
      traveling.climb.DismountCommand(), traveling.jump.JumpCommand(), traveling.move.MoveCommand(), 
      traveling.routes.RouteCommand(), traveling.travel.TravelCommand(), traveling.travel.TravelInDirectionCommand(), 
      use.UseCommand(), use.eat.EatCommand(), use.interaction.nothing.NothingCommand()
   )
}

What Changed?

  • Finding classes at runtime vs compile time
  • When reflection runs

sonic

Pros

  • Write once and forget
  • Follows the Unixy pattern of code generation
  • Decoupled
  • Reusable
  • Speed
  • Creativity

sonic

Cons

  • Additional complexity
  • Generated code / overriding code

sonic