diff --git a/frontends/sdl/src/main.rs b/frontends/sdl/src/main.rs
index c4137fda3bd93ae8eba5e5453b7331db0370959d..5ab6c7fcecbeb4497522fc92449d2031783c8b20 100644
--- a/frontends/sdl/src/main.rs
+++ b/frontends/sdl/src/main.rs
@@ -484,6 +484,8 @@ impl Emulator {
     }
 
     pub fn run_headless(&mut self, allowed_cycles: Option<u64>) {
+        let allowed_cycles = allowed_cycles.unwrap_or(u64::MAX);
+
         // starts the variable that will control the number of cycles that
         // are going to move (because of overflow) from one tick to another
         let mut pending_cycles = 0u32;
@@ -540,7 +542,7 @@ impl Emulator {
                 // fot the current tick an in case the total number of cycles
                 // exceeds the allowed cycles then the loop is broken
                 total_cycles += cycle_limit as u64;
-                if total_cycles >= allowed_cycles.unwrap_or(u64::MAX) {
+                if total_cycles >= allowed_cycles {
                     break;
                 }
 
@@ -635,7 +637,7 @@ fn main() {
 
     // creates a new Game Boy instance and loads both the boot ROM
     // and the initial game ROM to "start the engine"
-    let mut game_boy = GameBoy::new(mode);
+    let mut game_boy = GameBoy::new(Some(mode));
     let device = build_device(&args.device);
     game_boy.set_ppu_enabled(!args.no_ppu);
     game_boy.set_apu_enabled(!args.no_apu);
@@ -663,6 +665,10 @@ fn main() {
     emulator.start(SCREEN_SCALE);
     emulator.load_rom(Some(&args.rom_path));
     emulator.toggle_palette();
+
+    // determines if the emulator should run in headless mode or
+    // not and runs it accordingly, note that if running in headless
+    // mode the number of cycles to be run may be specified
     if args.headless {
         emulator.run_headless(if args.cycles > 0 {
             Some(args.cycles)
diff --git a/src/devices/buffer.rs b/src/devices/buffer.rs
new file mode 100644
index 0000000000000000000000000000000000000000..cf5707b71d5c296b6c58ea75e3235ac9fe2499d3
--- /dev/null
+++ b/src/devices/buffer.rs
@@ -0,0 +1,62 @@
+use crate::serial::SerialDevice;
+
+use std::fmt::{self, Display, Formatter};
+
+pub struct BufferDevice {
+    buffer: Vec<u8>,
+    callback: fn(image_buffer: &Vec<u8>),
+}
+
+impl BufferDevice {
+    pub fn new() -> Self {
+        Self {
+            buffer: Vec::new(),
+            callback: |_| {},
+        }
+    }
+
+    pub fn set_callback(&mut self, callback: fn(image_buffer: &Vec<u8>)) {
+        self.callback = callback;
+    }
+
+    pub fn buffer(&self) -> &Vec<u8> {
+        &self.buffer
+    }
+}
+
+impl SerialDevice for BufferDevice {
+    fn send(&mut self) -> u8 {
+        0xff
+    }
+
+    fn receive(&mut self, byte: u8) {
+        self.buffer.push(byte);
+        let data = vec![byte];
+        (self.callback)(&data);
+    }
+
+    fn allow_slave(&self) -> bool {
+        false
+    }
+
+    fn description(&self) -> String {
+        String::from("Buffer")
+    }
+
+    fn state(&self) -> String {
+        let buffer = self.buffer.clone();
+        String::from_utf8(buffer).unwrap()
+    }
+}
+
+impl Default for BufferDevice {
+    fn default() -> Self {
+        Self::new()
+    }
+}
+
+impl Display for BufferDevice {
+    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+        write!(f, "Buffer")
+    }
+}
diff --git a/src/devices/mod.rs b/src/devices/mod.rs
index ada19f9138884470af7746f6fa424080b2601f8a..339782b8cb892670253dea631cc9615038ff3bf1 100644
--- a/src/devices/mod.rs
+++ b/src/devices/mod.rs
@@ -1,2 +1,3 @@
+pub mod buffer;
 pub mod printer;
 pub mod stdout;
diff --git a/src/devices/printer.rs b/src/devices/printer.rs
index cf0161a8c90b319f39c6aa0fbe30e3210333b50d..dade2045f059a174201655d06af1322109ca3d07 100644
--- a/src/devices/printer.rs
+++ b/src/devices/printer.rs
@@ -316,6 +316,10 @@ impl SerialDevice for PrinterDevice {
     fn description(&self) -> String {
         format!("Printer [{}]", self.command)
     }
+
+    fn state(&self) -> String {
+        self.command.to_string()
+    }
 }
 
 impl Default for PrinterDevice {
diff --git a/src/devices/stdout.rs b/src/devices/stdout.rs
index 8a13eda05802a8246799935e050b8539263965dc..75dbcd27af02c480b82d454b25f887e2cd0316f3 100644
--- a/src/devices/stdout.rs
+++ b/src/devices/stdout.rs
@@ -7,7 +7,7 @@ use std::{
 
 pub struct StdoutDevice {
     flush: bool,
-    callback: fn(image_buffer: &Vec<u8>),
+    callback: fn(buffer: &Vec<u8>),
 }
 
 impl StdoutDevice {
@@ -18,7 +18,7 @@ impl StdoutDevice {
         }
     }
 
-    pub fn set_callback(&mut self, callback: fn(image_buffer: &Vec<u8>)) {
+    pub fn set_callback(&mut self, callback: fn(buffer: &Vec<u8>)) {
         self.callback = callback;
     }
 }
@@ -44,6 +44,10 @@ impl SerialDevice for StdoutDevice {
     fn description(&self) -> String {
         String::from("Stdout")
     }
+
+    fn state(&self) -> String {
+        String::from("")
+    }
 }
 
 impl Default for StdoutDevice {
diff --git a/src/gb.rs b/src/gb.rs
index 073fa08c99cd90eaf86a0e1e026fdb11dfecb50a..66c37c44ab0f421a9c49c36675ce53cfee813ba7 100644
--- a/src/gb.rs
+++ b/src/gb.rs
@@ -346,7 +346,8 @@ pub struct GameBoy {
 #[cfg_attr(feature = "wasm", wasm_bindgen)]
 impl GameBoy {
     #[cfg_attr(feature = "wasm", wasm_bindgen(constructor))]
-    pub fn new(mode: GameBoyMode) -> Self {
+    pub fn new(mode: Option<GameBoyMode>) -> Self {
+        let mode = mode.unwrap_or(GameBoyMode::Dmg);
         let gbc = Rc::new(RefCell::new(GameBoyConfig {
             mode,
             ppu_enabled: true,
@@ -1052,7 +1053,7 @@ impl AudioProvider for GameBoy {
 
 impl Default for GameBoy {
     fn default() -> Self {
-        Self::new(GameBoyMode::Dmg)
+        Self::new(None)
     }
 }
 
diff --git a/src/lib.rs b/src/lib.rs
index d25f5ac6f5082c43cfa6f03b76f2f88425e7bef6..0f8c9a49a05ec691e4c9fcacd0bc53de1e681123 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -14,5 +14,6 @@ pub mod pad;
 pub mod ppu;
 pub mod rom;
 pub mod serial;
+pub mod test;
 pub mod timer;
 pub mod util;
diff --git a/src/serial.rs b/src/serial.rs
index a8f522b77356aed07ad664fa41f1276f8d13094e..a9bd6205435c22187694d291eda6d56ffe1c9f8b 100644
--- a/src/serial.rs
+++ b/src/serial.rs
@@ -1,7 +1,11 @@
 use crate::warnln;
 
 pub trait SerialDevice {
+    /// Sends a byte (u8) to the attached serial connection.
     fn send(&mut self) -> u8;
+
+    /// Receives a byte (u8) from the attached serial connection,
+    /// can be either another device or the host.
     fn receive(&mut self, byte: u8);
 
     /// Whether the serial device "driver" supports slave mode
@@ -11,6 +15,10 @@ pub trait SerialDevice {
 
     /// Returns a short description of the serial device.
     fn description(&self) -> String;
+
+    /// Returns a string describing the current state of the
+    /// serial device. Could be used for debugging purposes.
+    fn state(&self) -> String;
 }
 
 pub struct Serial {
@@ -222,6 +230,10 @@ impl SerialDevice for NullDevice {
     fn description(&self) -> String {
         String::from("Null")
     }
+
+    fn state(&self) -> String {
+        String::from("")
+    }
 }
 
 impl Default for NullDevice {
diff --git a/src/test.rs b/src/test.rs
new file mode 100644
index 0000000000000000000000000000000000000000..39dc1dbab4a8901e77a4782470796f175e71c11e
--- /dev/null
+++ b/src/test.rs
@@ -0,0 +1,53 @@
+use crate::{devices::buffer::BufferDevice, gb::GameBoy};
+
+#[derive(Default)]
+pub struct TestOptions {
+    ppu_enabled: Option<bool>,
+    apu_enabled: Option<bool>,
+    dma_enabled: Option<bool>,
+    timer_enabled: Option<bool>,
+}
+
+pub fn build_test(options: TestOptions) -> GameBoy {
+    let device = Box::<BufferDevice>::default();
+    let mut game_boy = GameBoy::new(None);
+    game_boy.set_ppu_enabled(options.ppu_enabled.unwrap_or(true));
+    game_boy.set_apu_enabled(options.apu_enabled.unwrap_or(true));
+    game_boy.set_dma_enabled(options.dma_enabled.unwrap_or(true));
+    game_boy.set_timer_enabled(options.timer_enabled.unwrap_or(true));
+    game_boy.attach_serial(device);
+    game_boy.load(true);
+    game_boy
+}
+
+pub fn run_test(rom_path: &str, max_cycles: Option<u64>, options: TestOptions) -> String {
+    let mut cycles = 0u64;
+    let max_cycles = max_cycles.unwrap_or(u64::MAX);
+
+    let mut game_boy = build_test(options);
+    game_boy.load_rom_file(rom_path);
+
+    loop {
+        cycles += game_boy.clock() as u64;
+        if cycles >= max_cycles {
+            break;
+        }
+    }
+
+    game_boy.serial().device().state()
+}
+
+#[cfg(test)]
+mod tests {
+    use super::{run_test, TestOptions};
+
+    #[test]
+    fn test_blargg_cpu_instrs() {
+        let result = run_test(
+            "res/roms/test/blargg/cpu/cpu_instrs.gb",
+            Some(300000000),
+            TestOptions::default(),
+        );
+        assert_eq!(result, "cpu_instrs\n\n01:ok  02:ok  03:ok  04:ok  05:ok  06:ok  07:ok  08:ok  09:ok  10:ok  11:ok  \n\nPassed all tests\n");
+    }
+}