diff --git a/examples/fn.ion b/examples/fn.ion
index faac03ac1c87118598358acf5b4351f23efc65fc..f02a9d4787b1b32a1598502856a75c274a57717a 100644
--- a/examples/fn.ion
+++ b/examples/fn.ion
@@ -1,10 +1,10 @@
-fn test a b c
+fn first_test a b c
   echo $a
   echo $b
   echo $c
 end
 
-test hello world goodbye
+first_test hello world goodbye
 
 fn another_test
     for i in 1..10
diff --git a/src/builtins/mod.rs b/src/builtins/mod.rs
index 931fc761a3f7d499fd263b85b105e2ea33c7ad08..516417ce48d061655feed539ec0513c529d3753d 100644
--- a/src/builtins/mod.rs
+++ b/src/builtins/mod.rs
@@ -1,6 +1,8 @@
 pub mod source;
 pub mod variables;
 pub mod functions;
+
+mod test;
 mod echo;
 mod calc;
 
@@ -8,6 +10,7 @@ use self::variables::{alias, drop_alias, drop_variable, export_variable};
 use self::functions::fn_;
 use self::source::source;
 use self::echo::echo;
+use self::test::test;
 
 use fnv::FnvHashMap;
 use std::io::{self, Write};
@@ -233,6 +236,24 @@ impl Builtin {
                             }
                         });
 
+        commands.insert("test",
+                        Builtin {
+                            name: "test",
+                            help: "Performs tests on files and text",
+                            main: box |args: &[String], _: &mut Shell| -> i32 {
+                                match test(args) {
+                                    Ok(true) => SUCCESS,
+                                    Ok(false) => FAILURE,
+                                    Err(why) => {
+                                        let stderr = io::stderr();
+                                        let mut stderr = stderr.lock();
+                                        let _ = stderr.write_all(why.as_bytes());
+                                        FAILURE
+                                    }
+                                }
+                            }
+                        });
+
         commands.insert("calc",
                         Builtin {
                             name: "calc",
diff --git a/src/builtins/test.rs b/src/builtins/test.rs
new file mode 100644
index 0000000000000000000000000000000000000000..d2ea70116aefbc8211b0ccacedcf5f7d29f4ab3b
--- /dev/null
+++ b/src/builtins/test.rs
@@ -0,0 +1,462 @@
+use std::io::{self, Write, BufWriter};
+use std::fs;
+use std::path::Path;
+use std::os::unix::fs::{FileTypeExt, MetadataExt, PermissionsExt};
+use std::time::SystemTime;
+use std::error::Error;
+
+const MAN_PAGE: &'static str = /* @MANSTART{test} */ r#"
+NAME
+    test - perform tests on files and text
+
+SYNOPSIS
+    test [EXPRESSION]
+
+DESCRIPTION
+    Tests the expressions given and returns an exit status of 0 if true, else 1.
+
+OPTIONS
+    -n STRING
+        the length of STRING is nonzero
+
+    STRING
+        equivalent to -n STRING
+
+    -z STRING
+        the length of STRING is zero
+
+    STRING = STRING
+        the strings are equivalent
+
+    STRING != STRING
+        the strings are not equal
+
+    INTEGER -eq INTEGER
+        the integers are equal
+
+    INTEGER -ge INTEGER
+        the first INTEGER is greater than or equal to the first INTEGER
+
+    INTEGER -gt INTEGER
+        the first INTEGER is greater than the first INTEGER
+
+    INTEGER -le INTEGER
+        the first INTEGER is less than or equal to the first INTEGER
+
+    INTEGER -lt INTEGER
+        the first INTEGER is less than the first INTEGER
+
+    INTEGER -ne INTEGER
+        the first INTEGER is not equal to the first INTEGER
+
+    FILE -ef FILE
+        both files have the same device and inode numbers
+
+    FILE -nt FILE
+        the first FILE is newer than the second FILE
+
+    FILE -ot FILE
+        the first file is older than the second FILE
+
+    -b FILE
+        FILE exists and is a block device
+
+    -c FILE
+        FILE exists and is a character device
+
+    -d FILE
+        FILE exists and is a directory
+
+    -e FILE
+        FILE exists
+
+    -f FILE
+        FILE exists and is a regular file
+
+    -h FILE
+        FILE exists and is a symbolic link (same as -L)
+
+    -L FILE
+        FILE exists and is a symbolic link (same as -h)
+
+    -r FILE
+        FILE exists and read permission is granted
+
+    -s FILE
+        FILE exists and has a file size greater than zero
+
+    -S FILE
+        FILE exists and is a socket
+
+    -w FILE
+        FILE exists and write permission is granted
+
+    -x FILE
+        FILE exists and execute (or search) permission is granted
+
+EXAMPLES
+    Test if the file exists:
+        test -e FILE && echo "The FILE exists" || echo "The FILE does not exist"
+
+    Test if the file exists and is a regular file, and if so, write to it:
+        test -f FILE && echo "Hello, FILE" >> FILE || echo "Cannot write to a directory"
+
+    Test if 10 is greater than 5:
+        test 10 -gt 5 && echo "10 is greater than 5" || echo "10 is not greater than 5"
+
+    Test if the user is running a 64-bit OS (POSIX environment only):
+        test $(getconf LONG_BIT) = 64 && echo "64-bit OS" || echo "32-bit OS"
+
+AUTHOR
+    Written by Michael Murphy.
+"#; /* @MANEND */
+
+pub fn test(args: &[String]) -> Result<bool, String> {
+    let stdout = io::stdout();
+    let mut buffer = BufWriter::new(stdout.lock());
+    
+    let arguments = &args[1..];
+    evaluate_arguments(arguments, &mut buffer)
+}
+
+fn evaluate_arguments(arguments: &[String], buffer: &mut BufWriter<io::StdoutLock>) -> Result<bool, String> {
+    if let Some(arg) = arguments.first() {
+        if arg.as_str() == "--help" {
+            buffer.write_all(MAN_PAGE.as_bytes()).map_err(|x| x.description().to_owned())?;
+            buffer.flush().map_err(|x| x.description().to_owned())?;
+
+            return Ok(true);
+        }
+        let mut characters = arg.chars().take(2);
+        return match characters.next().unwrap() {
+            '-' => {
+                // If no flag was given, return `SUCCESS`
+                characters.next().map_or(Ok(true), |flag| {
+                    // If no argument was given, return `SUCCESS`
+                    arguments.get(1).map_or(Ok(true), |argument| {
+                        // match the correct function to the associated flag
+                        Ok(match_flag_argument(flag, argument.as_str()))
+                    })
+                })
+            },
+            _   => {
+                // If there is no operator, check if the first argument is non-zero
+                arguments.get(1).map_or(Ok(string_is_nonzero(&arg)), |operator| {
+                    // If there is no right hand argument, a condition was expected
+                    let right_arg = arguments.get(2).ok_or(String::from("parse error: condition expected"))?;
+                    evaluate_expression(arg.as_str(), operator.as_str(), right_arg.as_str())
+                })
+            },
+        };
+    } else {
+        return Ok(false);
+    }
+}
+
+fn evaluate_expression(first: &str, operator: &str, second: &str) -> Result<bool, String> {
+    match operator {
+        "=" | "==" => Ok(evaluate_bool(first == second)),
+        "!="       => Ok(evaluate_bool(first != second)),
+        "-ef"      => Ok(files_have_same_device_and_inode_numbers(first, second)),
+        "-nt"      => Ok(file_is_newer_than(first, second)),
+        "-ot"      => Ok(file_is_newer_than(second, first)),
+        _          => {
+            let (left, right) = parse_integers(first, second)?;
+            match operator {
+                "-eq" => Ok(evaluate_bool(left == right)),
+                "-ge" => Ok(evaluate_bool(left >= right)),
+                "-gt" => Ok(evaluate_bool(left > right)),
+                "-le" => Ok(evaluate_bool(left <= right)),
+                "-lt" => Ok(evaluate_bool(left < right)),
+                "-ne" => Ok(evaluate_bool(left != right)),
+                _     => {
+                    Err(format!("unknowne condition: {:?}", operator))
+                }
+            }
+        }
+    }
+
+}
+
+/// Exits SUCCESS if both files have the same device and inode numbers
+fn files_have_same_device_and_inode_numbers(first: &str, second: &str) -> bool {
+    // Obtain the device and inode of the first file or return FAILED
+    get_dev_and_inode(first).map_or(false, |left| {
+        // Obtain the device and inode of the second file or return FAILED
+        get_dev_and_inode(second).map_or(false, |right| {
+            // Compare the device and inodes of the first and second files
+            evaluate_bool(left == right)
+        })
+    })
+}
+
+/// Obtains the device and inode numbers of the file specified
+fn get_dev_and_inode(filename: &str) -> Option<(u64, u64)> {
+    fs::metadata(filename).map(|file| (file.dev(), file.ino())).ok()
+}
+
+/// Exits SUCCESS if the first file is newer than the second file.
+fn file_is_newer_than(first: &str, second: &str) -> bool {
+    // Obtain the modified file time of the first file or return FAILED
+    get_modified_file_time(first).map_or(false, |left| {
+        // Obtain the modified file time of the second file or return FAILED
+        get_modified_file_time(second).map_or(false, |right| {
+            // If the first file is newer than the right file, return SUCCESS
+            evaluate_bool(left > right)
+        })
+    })
+}
+
+/// Obtain the time the file was last modified as a `SystemTime` type.
+fn get_modified_file_time(filename: &str) -> Option<SystemTime> {
+    fs::metadata(filename).ok().and_then(|file| file.modified().ok())
+}
+
+/// Attempt to parse a &str as a usize.
+fn parse_integers(left: &str, right: &str) -> Result<(Option<usize>, Option<usize>), String> {
+    let parse_integer = |input: &str| -> Result<Option<usize>, String> {
+        match input.parse::<usize>().map_err(|_| {
+            format!("integer expression expected: {:?}", input)
+        }) {
+            Err(why) => Err(String::from(why)),
+            Ok(res) => Ok(Some(res)),
+        }
+    };
+
+    match (parse_integer(left), parse_integer(right)) {
+        (Err(left), Err(_)) => Err(left),
+        (Ok(_), Err(right)) => Err(right),
+        (Err(left), Ok(_)) => Err(left),
+        (Ok(left), Ok(right)) => Ok((left, right)),
+    }
+}
+
+/// Matches flag arguments to their respective functionaity when the `-` character is detected.
+fn match_flag_argument(flag: char, argument: &str) -> bool {
+    // TODO: Implement missing flags
+    match flag {
+        'b' => file_is_block_device(argument),
+        'c' => file_is_character_device(argument),
+        'd' => file_is_directory(argument),
+        'e' => file_exists(argument),
+        'f' => file_is_regular(argument),
+        //'g' => file_is_set_group_id(argument),
+        //'G' => file_is_owned_by_effective_group_id(argument),
+        'h' | 'L' => file_is_symlink(argument),
+        //'k' => file_has_sticky_bit(argument),
+        //'O' => file_is_owned_by_effective_user_id(argument),
+        //'p' => file_is_named_pipe(argument),
+        'r' => file_has_read_permission(argument),
+        's' => file_size_is_greater_than_zero(argument),
+        'S' => file_is_socket(argument),
+        //'t' => file_descriptor_is_opened_on_a_terminal(argument),
+        'w' => file_has_write_permission(argument),
+        'x' => file_has_execute_permission(argument),
+        'n' => string_is_nonzero(argument),
+        'z' => string_is_zero(argument),
+        _ => true,
+    }
+}
+
+/// Exits SUCCESS if the file size is greather than zero.
+fn file_size_is_greater_than_zero(filepath: &str) -> bool {
+    fs::metadata(filepath).ok().map_or(false, |metadata| evaluate_bool(metadata.len() > 0))
+}
+
+/// Exits SUCCESS if the file has read permissions. This function is rather low level because
+/// Rust currently does not have a higher level abstraction for obtaining non-standard file modes.
+/// To extract the permissions from the mode, the bitwise AND operator will be used and compared
+/// with the respective read bits.
+fn file_has_read_permission(filepath: &str) -> bool {
+    const USER_BIT:  u32 = 0b100000000;
+    const GROUP_BIT: u32 = 0b100000;
+    const GUEST_BIT: u32 = 0b100;
+
+    // Collect the mode of permissions for the file
+    fs::metadata(filepath).map(|metadata| metadata.permissions().mode()).ok()
+        // If the mode is equal to any of the above, return `SUCCESS`
+        .map_or(false, |mode| {
+            if mode & USER_BIT == USER_BIT || mode & GROUP_BIT == GROUP_BIT ||
+                mode & GUEST_BIT == GUEST_BIT { true } else { false }
+        })
+}
+
+/// Exits SUCCESS if the file has write permissions. This function is rather low level because
+/// Rust currently does not have a higher level abstraction for obtaining non-standard file modes.
+/// To extract the permissions from the mode, the bitwise AND operator will be used and compared
+/// with the respective write bits.
+fn file_has_write_permission(filepath: &str) -> bool {
+    const USER_BIT:  u32 = 0b10000000;
+    const GROUP_BIT: u32 = 0b10000;
+    const GUEST_BIT: u32 = 0b10;
+
+    // Collect the mode of permissions for the file
+    fs::metadata(filepath).map(|metadata| metadata.permissions().mode()).ok()
+        // If the mode is equal to any of the above, return `SUCCESS`
+        .map_or(false, |mode| {
+            if mode & USER_BIT == USER_BIT || mode & GROUP_BIT == GROUP_BIT ||
+                mode & GUEST_BIT == GUEST_BIT { true } else { false }
+        })
+}
+
+/// Exits SUCCESS if the file has execute permissions. This function is rather low level because
+/// Rust currently does not have a higher level abstraction for obtaining non-standard file modes.
+/// To extract the permissions from the mode, the bitwise AND operator will be used and compared
+/// with the respective execute bits.
+fn file_has_execute_permission(filepath: &str) -> bool {
+    const USER_BIT:  u32 = 0b1000000;
+    const GROUP_BIT: u32 = 0b1000;
+    const GUEST_BIT: u32 = 0b1;
+
+    // Collect the mode of permissions for the file
+    fs::metadata(filepath).map(|metadata| metadata.permissions().mode()).ok()
+        // If the mode is equal to any of the above, return `SUCCESS`
+        .map_or(false, |mode| {
+            if mode & USER_BIT == USER_BIT || mode & GROUP_BIT == GROUP_BIT ||
+                mode & GUEST_BIT == GUEST_BIT { true } else { false }
+        })
+}
+
+/// Exits SUCCESS if the file argument is a socket
+fn file_is_socket(filepath: &str) -> bool {
+    fs::metadata(filepath).ok()
+        .map_or(false, |metadata| evaluate_bool(metadata.file_type().is_socket()))
+}
+
+/// Exits SUCCESS if the file argument is a block device
+fn file_is_block_device(filepath: &str) -> bool {
+    fs::metadata(filepath).ok()
+        .map_or(false, |metadata| evaluate_bool(metadata.file_type().is_block_device()))
+}
+
+/// Exits SUCCESS if the file argument is a character device
+fn file_is_character_device(filepath: &str) -> bool {
+    fs::metadata(filepath).ok()
+        .map_or(false, |metadata| evaluate_bool(metadata.file_type().is_char_device()))
+}
+
+/// Exits SUCCESS if the file exists
+fn file_exists(filepath: &str) -> bool {
+    evaluate_bool(Path::new(filepath).exists())
+}
+
+/// Exits SUCCESS if the file is a regular file
+fn file_is_regular(filepath: &str) -> bool {
+    fs::metadata(filepath).ok()
+        .map_or(false, |metadata| evaluate_bool(metadata.file_type().is_file()))
+}
+
+/// Exits SUCCESS if the file is a directory
+fn file_is_directory(filepath: &str) -> bool {
+    fs::metadata(filepath).ok()
+        .map_or(false, |metadata| evaluate_bool(metadata.file_type().is_dir()))
+}
+
+/// Exits SUCCESS if the file is a symbolic link
+fn file_is_symlink(filepath: &str) -> bool {
+    fs::symlink_metadata(filepath).ok()
+        .map_or(false, |metadata| evaluate_bool(metadata.file_type().is_symlink()))
+}
+
+/// Exits SUCCESS if the string is not empty
+fn string_is_nonzero(string: &str) -> bool {
+    evaluate_bool(!string.is_empty())
+}
+
+/// Exits SUCCESS if the string is empty
+fn string_is_zero(string: &str) -> bool {
+    evaluate_bool(string.is_empty())
+}
+
+/// Convert a boolean to it's respective exit code.
+fn evaluate_bool(input_is_true: bool) -> bool { if input_is_true { true } else { false } }
+
+#[test]
+fn test_strings() {
+    assert_eq!(string_is_zero("NOT ZERO"), false);
+    assert_eq!(string_is_zero(""), true);
+    assert_eq!(string_is_nonzero("NOT ZERO"), true);
+    assert_eq!(string_is_nonzero(""), false);
+}
+
+#[test]
+fn test_integers_arguments() {
+    let stdout = io::stdout();
+    let mut buffer = BufWriter::new(stdout.lock());
+
+    // Equal To
+    assert_eq!(evaluate_arguments(&[String::from("10"), String::from("-eq"), String::from("10")],
+        &mut buffer), Ok(true));
+    assert_eq!(evaluate_arguments(&[String::from("10"), String::from("-eq"), String::from("5")],
+        &mut buffer), Ok(false));
+
+    // Greater Than or Equal To
+    assert_eq!(evaluate_arguments(&[String::from("10"), String::from("-ge"), String::from("10")],
+        &mut buffer), Ok(true));
+    assert_eq!(evaluate_arguments(&[String::from("10"), String::from("-ge"), String::from("5")],
+        &mut buffer), Ok(true));
+    assert_eq!(evaluate_arguments(&[String::from("5"), String::from("-ge"), String::from("10")],
+        &mut buffer), Ok(false));
+
+    // Less Than or Equal To
+    assert_eq!(evaluate_arguments(&[String::from("5"), String::from("-le"), String::from("5")],
+        &mut buffer), Ok(true));
+    assert_eq!(evaluate_arguments(&[String::from("5"), String::from("-le"), String::from("10")],
+        &mut buffer), Ok(true));
+    assert_eq!(evaluate_arguments(&[String::from("10"), String::from("-le"), String::from("5")],
+        &mut buffer), Ok(false));
+
+    // Less Than
+    assert_eq!(evaluate_arguments(&[String::from("5"), String::from("-lt"), String::from("10")],
+        &mut buffer), Ok(true));
+    assert_eq!(evaluate_arguments(&[String::from("10"), String::from("-lt"), String::from("5")],
+        &mut buffer), Ok(false));
+
+    // Greater Than
+    assert_eq!(evaluate_arguments(&[String::from("10"), String::from("-gt"), String::from("5")],
+        &mut buffer), Ok(true));
+    assert_eq!(evaluate_arguments(&[String::from("5"), String::from("-gt"), String::from("10")],
+        &mut buffer), Ok(false));
+
+    // Not Equal To
+    assert_eq!(evaluate_arguments(&[String::from("10"), String::from("-ne"), String::from("5")],
+        &mut buffer), Ok(true));
+    assert_eq!(evaluate_arguments(&[String::from("5"), String::from("-ne"), String::from("5")],
+        &mut buffer), Ok(false));
+}
+
+#[test]
+fn test_file_exists() {
+    assert_eq!(file_exists("testing/empty_file"), true);
+    assert_eq!(file_exists("this-does-not-exist"), false);
+}
+
+#[test]
+fn test_file_is_regular() {
+    assert_eq!(file_is_regular("testing/empty_file"), true);
+    assert_eq!(file_is_regular("testing"), false);
+}
+
+#[test]
+fn test_file_is_directory() {
+    assert_eq!(file_is_directory("testing"), true);
+    assert_eq!(file_is_directory("testing/empty_file"), false);
+}
+
+#[test]
+fn test_file_is_symlink() {
+    assert_eq!(file_is_symlink("testing/symlink"), true);
+    assert_eq!(file_is_symlink("testing/empty_file"), false);
+}
+
+#[test]
+fn test_file_has_execute_permission() {
+    assert_eq!(file_has_execute_permission("testing/executable_file"), true);
+    assert_eq!(file_has_execute_permission("testing/empty_file"), false);
+}
+
+#[test]
+fn test_file_size_is_greater_than_zero() {
+    assert_eq!(file_size_is_greater_than_zero("testing/file_with_text"), true);
+    assert_eq!(file_size_is_greater_than_zero("testing/empty_file"), false);
+}
diff --git a/src/shell/job.rs b/src/shell/job.rs
index 5d6639a328884001813140fb1292dc4a33d30d60..93580aa57e9fc3830a770f59d2485defa65c46df 100644
--- a/src/shell/job.rs
+++ b/src/shell/job.rs
@@ -79,7 +79,7 @@ enum CommandType {
 impl<'a> From<&'a str> for CommandType {
     fn from(command: &'a str) -> CommandType {
         match command {
-            "help" | "history" | "echo" | "calc" => CommandType::Builtin,
+            "help" | "history" | "echo" | "test" | "calc" => CommandType::Builtin,
             _ => CommandType::External
         }
     }
diff --git a/testing/empty_file b/testing/empty_file
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/testing/executable_file b/testing/executable_file
new file mode 100755
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/testing/file_with_text b/testing/file_with_text
new file mode 100644
index 0000000000000000000000000000000000000000..a843aad0c70eb4c75e0ac54db64dff3bc3ba9f4b
--- /dev/null
+++ b/testing/file_with_text
@@ -0,0 +1 @@
+FILE IS NOT EMPTY
diff --git a/testing/symlink b/testing/symlink
new file mode 120000
index 0000000000000000000000000000000000000000..47532f2469fe82640dd9b9d74e22368c8fd872db
--- /dev/null
+++ b/testing/symlink
@@ -0,0 +1 @@
+testing/empty_file
\ No newline at end of file