diff --git a/tests/test_controller/test_util.py b/tests/test_controller/test_util.py
new file mode 100644
index 0000000000000000000000000000000000000000..8c660a5c5cb31e023273c3b3c997c806668583b9
--- /dev/null
+++ b/tests/test_controller/test_util.py
@@ -0,0 +1,343 @@
+"""Test suite for functions defined in sohstationviewer.controller.util."""
+
+import os
+
+from unittest import TestCase
+from unittest.mock import patch
+from tempfile import TemporaryDirectory, NamedTemporaryFile
+import string
+
+
+from sohstationviewer.controller.util import (
+    validateFile,
+    getDirSize,
+    getTime6,
+    getTime6_2y,
+    getTime6_4y,
+    getTime4,
+    getVal,
+    rtnPattern,
+    fmti
+)
+
+TEST_DATA_DIR = os.path.realpath(os.path.join(
+    os.path.dirname(os.path.realpath(__file__)),
+    os.pardir,
+    'test_data',
+))
+
+
+class TestGetTime(TestCase):
+    """Test suite for getTime6, getTime6_2y, getTime6_4y, and getTime4."""
+    def setUp(self):
+        """Set up text fixtures."""
+        self.time6_2y = '01:251:09:41:35:656'
+        self.time6_4y = '2001:251:09:41:35:656'
+        self.time4_day_1 = '1:09:41:35'
+        self.time4 = '251:09:41:35'
+
+    @patch('sohstationviewer.controller.util.getTime6_4y')
+    @patch('sohstationviewer.controller.util.getTime6_2y')
+    def test_get_time6(self, mock_2y, mock_4y):
+        """
+        Test getTime6 - check that getTime6 delegates work to the appropriate
+        helper function depending on the input.
+        """
+        with self.subTest('test_2_digit_year'):
+            getTime6(self.time6_2y)
+            self.assertTrue(mock_2y.called)
+            self.assertFalse(mock_4y.called)
+        mock_2y.reset_mock()
+        mock_4y.reset_mock()
+        with self.subTest('test_4_digit_year'):
+            getTime6(self.time6_4y)
+            self.assertTrue(mock_4y.called)
+            self.assertFalse(mock_2y.called)
+
+    def test_get_time6_invalid_input(self):
+        """Test getTime6 - the input is not one of the expected formats."""
+        with self.subTest('test_input_contains_colon'):
+            bad_inputs = [':523:531:', 'fs:523:531:', 'towe:523:531:']
+            for input_str in bad_inputs:
+                with self.assertRaises(ValueError):
+                    getTime6(input_str)
+        with self.subTest('test_input_does_not_contain_colon'):
+            input_str = 'fq31dqrt63'
+            with self.assertRaises(ValueError):
+                getTime6(input_str)
+
+    def test_get_time6_2y(self):
+        """Test getTime6_2y."""
+        epoch_time, year = getTime6_2y(self.time6_2y)
+        self.assertAlmostEqual(epoch_time, 999942095.656)
+        self.assertEqual(year, 2001)
+
+    def test_get_time6_4y(self):
+        """Test getTime6_4y."""
+        epoch_time, year = getTime6_4y(self.time6_4y)
+        self.assertAlmostEqual(epoch_time, 999942095.656)
+        self.assertEqual(year, 2001)
+
+    def test_get_time4_year_added(self):
+        """Test getTime4 - a year has been added."""
+        year = 2001
+        year_added = True
+        with self.subTest('test_first_day_of_year'):
+            epoch_time, ret_year, ret_year_added = (
+                getTime4(self.time4_day_1, year, year_added)
+            )
+            self.assertEqual(epoch_time, 978342095)
+            self.assertEqual(ret_year, year)
+            self.assertTrue(ret_year_added)
+
+        with self.subTest('test_other_days'):
+            epoch_time, ret_year, ret_year_added = (
+                getTime4(self.time4, year, year_added)
+            )
+            self.assertEqual(epoch_time, 999942095)
+            self.assertEqual(ret_year, year)
+            self.assertTrue(ret_year_added)
+
+    def test_get_time4_year_not_added(self):
+        """Test getTime4 - a year has not been added."""
+        year = 2001
+        year_added = False
+        with self.subTest('test_first_day_of_year'):
+            epoch_time, ret_year, ret_year_added = (
+                getTime4(self.time4_day_1, year, year_added)
+            )
+            self.assertEqual(epoch_time, 1009878095)
+            self.assertEqual(ret_year, year + 1)
+            self.assertTrue(ret_year_added)
+
+        with self.subTest('test_other_days'):
+            epoch_time, ret_year, ret_year_added = (
+                getTime4(self.time4, year, year_added)
+            )
+            self.assertEqual(epoch_time, 999942095)
+            self.assertEqual(ret_year, year)
+            self.assertFalse(ret_year_added)
+
+
+class TestValidateFile(TestCase):
+    """Test suite for validateFile."""
+    def test_valid_file(self):
+        """
+        Test basic functionality of validateFile - given file exists and is not
+        an info file.
+        """
+        with NamedTemporaryFile() as valid_file:
+            self.assertTrue(
+                validateFile(valid_file.name,
+                             os.path.basename(valid_file.name))
+            )
+
+    def test_info_file(self):
+        """
+        Test basic functionality of validateFile - given file exists and is an
+        info file.
+        """
+        with self.subTest('test_dot_DS_Store'):
+            with TemporaryDirectory() as temp_dir:
+                # There are two ways to create a temporary file with a
+                # custom name. The first way is to create a temporary
+                # directory, then create a file in that directory. When the
+                # temporary directory is deleted, the file is also deleted.
+                # This automatic deletion effectively makes the file we created
+                # a temporary file.
+                # The second way is to create a temporary file and rename it
+                # to the name we want. Then, we need to rename the file back
+                # to its original name once we are done. This renaming is
+                # needed because temporary file deletes itself based on its
+                # name attribute, which is not changed in the first
+                # renaming. While the second way makes it explicit that we
+                # only want a temporary file, it might not be clear why the
+                # second renaming is needed. In order to cause less
+                # confusion in the code, we create this temporary file the
+                # first way.
+                ds_store_path = os.path.join(temp_dir, '.DS_Store')
+                with open(ds_store_path, 'w+') as ds_store_file:
+                    self.assertFalse(
+                        validateFile(ds_store_path,
+                                     os.path.basename(ds_store_file.name))
+                    )
+
+        with self.subTest('test_dot_underscore'):
+            with NamedTemporaryFile(prefix='._') as info_file:
+                self.assertFalse(
+                    validateFile(info_file.name,
+                                 os.path.basename(info_file.name))
+                )
+
+    def test_file_does_not_exist(self):
+        """
+        Test basic functionality of validateFile - given file does not exist.
+        """
+        empty_name_file = ''
+        self.assertFalse(validateFile(empty_name_file, empty_name_file))
+
+        not_exist_file = 'file_does_not_exist'
+        self.assertFalse(validateFile(not_exist_file, not_exist_file))
+
+
+class TestGetDirSize(TestCase):
+    """Test suite for getDirSize."""
+    def test_files_have_size_zero(self):
+        """Test getDirSize - all files in the given directory has size zero."""
+        expected_file_count = 10
+        with TemporaryDirectory() as directory:
+            files = []
+            for i in range(expected_file_count):
+                files.append(NamedTemporaryFile(dir=directory))
+            dir_size, dir_file_count = getDirSize(directory)
+            self.assertEqual(dir_size, 0)
+            self.assertEqual(dir_file_count, expected_file_count)
+            # Explicitly clean up the temporary files. If we don't do this,
+            # the temporary directory will clean up itself and delete the
+            # temporary files. Then, when the function returns, the references
+            # to these temporary files will attempt to clean up the files. This
+            # leads to exceptions being raised because the files being cleaned
+            # up does not exist any more.
+            files = [file.close() for file in files]
+
+    def test_files_have_size_greater_than_zero(self):
+        """
+        Test getDirSize - all files in the given directory have a size greater
+        than zero.
+        """
+        expected_file_count = 10
+        size_one_file = 10
+        # Each byte character is 1 byte, so we can obtain a byte string of n
+        # bytes by creating a byte string with n characters.
+        byte_to_write = b'a' * size_one_file
+        temp_dir = TemporaryDirectory()
+        with TemporaryDirectory(dir=temp_dir.name) as directory:
+            files = []
+            for i in range(expected_file_count):
+                temp_file = NamedTemporaryFile(dir=directory)
+                temp_file.write(byte_to_write)
+                # Writing to file is not done until requested so we need to
+                # explicitly tell Python to finish the write. Otherwise, the
+                # size of the file on disk will stay 0.
+                temp_file.flush()
+                files.append(temp_file)
+            dir_size, dir_file_count = getDirSize(temp_dir.name)
+            self.assertEqual(dir_size, size_one_file * expected_file_count)
+            self.assertEqual(dir_file_count, expected_file_count)
+            # Explicitly clean up the temporary files. If we don't do this,
+            # the temporary directory will clean up itself and delete the
+            # temporary files. Then, when the function returns, the references
+            # to these temporary files will attempt to clean up the files. This
+            # leads to exceptions being raised because the files being cleaned
+            # up does not exist any more.
+            files = [file.close() for file in files]
+
+    def test_nested_folder_structure(self):
+        """
+        Test getDirSize - the given directory contains nested directories.
+        """
+        test_folder = os.path.join(TEST_DATA_DIR, 'Pegasus-sample')
+        dir_size, dir_file_count = getDirSize(test_folder)
+        self.assertEqual(dir_size, 7909115)
+        self.assertEqual(dir_file_count, 8)
+
+    def test_empty_directory(self):
+        """Test getDirSize - the given directory contains no file."""
+        with TemporaryDirectory() as temp_dir:
+            dir_size, dir_file_count = getDirSize(temp_dir)
+            self.assertEqual(dir_size, 0)
+            self.assertEqual(dir_file_count, 0)
+
+    def test_directory_does_not_exist(self):
+        """Test getDirSize - the given directory does not exist."""
+        empty_name_dir = ''
+        dir_size, dir_file_count = getDirSize(empty_name_dir)
+        self.assertEqual(dir_size, 0)
+        self.assertEqual(dir_file_count, 0)
+
+        non_existent_dir = 'directory does not exist'
+        dir_size, dir_file_count = getDirSize(non_existent_dir)
+        self.assertEqual(dir_size, 0)
+        self.assertEqual(dir_file_count, 0)
+
+
+class TestRtnPattern(TestCase):
+    """Test suite for rtnPattern."""
+    def test_no_upper(self):
+        """Test rtnPattern - characters are not converted to uppercase."""
+        with self.subTest('test_digit'):
+            digits = '123456789'
+            self.assertEqual(rtnPattern(digits), '0' * len(digits))
+        with self.subTest('test_lowercase'):
+            lowercase_chars = string.ascii_lowercase
+            self.assertEqual(rtnPattern(lowercase_chars),
+                             'a' * len(lowercase_chars))
+        with self.subTest('test_uppercase'):
+            uppercase_chars = string.ascii_uppercase
+            self.assertEqual(rtnPattern(uppercase_chars),
+                             'A' * len(uppercase_chars))
+
+    def test_with_upper(self):
+        """Test rtnPattern - all characters are converted to uppercase."""
+        lowercase_chars = string.ascii_lowercase
+        self.assertEqual(rtnPattern(lowercase_chars, upper=True),
+                         'A' * len(lowercase_chars))
+
+
+class TestGetVal(TestCase):
+    """Test suite for getVal."""
+    def test_normal_case(self):
+        """Test getVal - the input is of an expected value."""
+        # formatter:off
+        test_name_to_test_map = {
+            'test_with_decimal_point':      ('60.3V',   60.3),
+            'test_no_decimal_point':        ('15V',     15.0),
+            'test_with_plus_sign':          ('+35.5V',  35.5),
+            'test_with_negative_sign':      ('-35.2V', -35.2),
+            'test_multiple_decimal_digits': ('52.523V', 52.5),
+            'test_no_decimal_digit':        ('12.V',    12.0),
+        }
+        # formatter:on
+        for test_name, inout_pair in test_name_to_test_map.items():
+            with self.subTest(test_name):
+                self.assertEqual(getVal(inout_pair[0]), inout_pair[1])
+
+    def test_positive_negative_sign_in_front(self):
+        """
+        Test rtnPattern - the input has both a positive sign and a negative
+        sign in the front.
+        """
+        with self.assertRaises(ValueError):
+            getVal('+-1.0V')
+
+    def test_bad_input(self):
+        """Test rtnPattern - the input has a value that is not expected."""
+        with self.assertRaises(AttributeError):
+            getVal('')
+
+
+class TestFmti(TestCase):
+    """Test suite for fmti."""
+    def test_absolute_value_below_1000(self):
+        """
+        Test fmti - the input is greater than -1000 but smaller than 1000.
+        """
+        with self.subTest('test_positive'):
+            val = 52.521
+            self.assertEqual(fmti(val), '52')
+        with self.subTest('test_negative'):
+            val = -232.42
+            self.assertEqual(fmti(val), '-232')
+
+    def test_absolute_value_above_1000(self):
+        """
+        Test fmti - the input is greater than 1000 or smaller than -1000.
+        """
+        with self.subTest('test_positive'):
+            val = 136235646.215151
+            expected = '136,235,646'
+            self.assertEqual(fmti(val), expected)
+        with self.subTest('test_negative'):
+            val = -62362.32523
+            expected = '-62,362'
+            self.assertEqual(fmti(val), expected)