diff --git a/AUTHORS.rst b/AUTHORS.rst
index 8029b2489bd99e83141f2a6e55f9aeb1f9c6f30c..207d6dd429fa08cf357717a87a587314d3012b29 100644
--- a/AUTHORS.rst
+++ b/AUTHORS.rst
@@ -10,4 +10,4 @@ Development Lead
 Contributors
 ------------
 
-None yet. Why not be the first?
+* Maeva Pourpoint <software-support@passcal.nmt.edu>
diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst
index e8536cc925d7042cd527fed67034e6b6781936cf..b262255e1da62b498a111fdf7990ac7c80c4afdb 100644
--- a/CONTRIBUTING.rst
+++ b/CONTRIBUTING.rst
@@ -76,7 +76,7 @@ Ready to contribute? Here's how to set up `data2passcal` for local development.
 5. When you're done making changes, check that your changes pass the
    tests::
 
-    $ python setup.py test
+    $ python -m pytest
 6. Commit your changes and push your branch to GitHub::
 
     $ git add .
@@ -111,4 +111,3 @@ Then run::
 
 $ git push
 $ git push --tags
-
diff --git a/HISTORY.rst b/HISTORY.rst
index db916b9f439e3d1737299b77f75377f8c715b676..d35c92df96d952c9a569f2e4cdf40715dd450ed9 100644
--- a/HISTORY.rst
+++ b/HISTORY.rst
@@ -17,3 +17,11 @@ History
 * Resources used: Python-Future project and  Futurize tool.
 * Changes tested using unittest module on individual test methods(tests.test_data2passcal)
 * Changes tested against real seismic dataset (AB.2002 - 2012)
+
+2020.107 (2020-04-16)
+------------------
+* Added some unit tests to ensure basic functionality of data2passcal
+* Created configuration file (.ini file) to store and access ftp and test related variables
+* Updated list of platform specific dependencies to be installed when installing data2passcal (see setup.py)
+* Installed and tested data2passcal against Python2.7 and Python3.6 using tox
+* Formatted Python code to conform to the PEP8 style guide (exception: E722, E712, E501)
diff --git a/README.rst b/README.rst
index efc03cf70b5aeeb77a45be7fff9dd765383a0534..a1160b99c3905a79cfa2cd8fa0e04228ddbc9968 100644
--- a/README.rst
+++ b/README.rst
@@ -3,23 +3,9 @@ data2passcal
 ============
 
 
-Prepare SEED data for shipment to PASSCAL.
-Only accept day-long MSEED files.
+Description: Send MSEED files ready for archival at the DMC to PASSCAL's QC system
+             Only accept day-long MSEED files.
+Usage:       data2passcal dir
 
 
 * Free software: GNU General Public License v3 (GPLv3)
-
-
-
-Features
---------
-
-* TODO
-
-Credits
--------
-
-This package was created with Cookiecutter_ and the `passoft/cookiecutter`_ project template.
-
-.. _Cookiecutter: https://github.com/audreyr/cookiecutter
-.. _`passoft/cookiecutter`: https://git.passcal.nmt.edu/passoft/cookiecutter
diff --git a/data2passcal/__init__.py b/data2passcal/__init__.py
index afe21e9225139b7e746aabc93df87e9991ff3b07..8f21cef6e0f1a9f038e49585d5d448812b2883df 100644
--- a/data2passcal/__init__.py
+++ b/data2passcal/__init__.py
@@ -4,4 +4,4 @@
 
 __author__ = """IRIS PASSCAL"""
 __email__ = 'software-support@passcal.nmt.edu'
-__version__ = '2020.093'
+__version__ = '2020.107'
diff --git a/data2passcal/config.ini b/data2passcal/config.ini
new file mode 100644
index 0000000000000000000000000000000000000000..6422e381c6501486395a1a03217b97da2d96db20
--- /dev/null
+++ b/data2passcal/config.ini
@@ -0,0 +1,22 @@
+[FTP_INFO]
+ftp_ip = 129.138.26.29
+ftp_user = ftp
+ftp_password = data2passcal
+ftp_send_attempts = 3
+ftp_blocksize = 8192
+ftp_timeout = 120
+ftp_dir = AUTO/MSEED
+ftp_reconnect_wait = 60
+ftp_debug_level = 0
+ftp_host = qc.passcal.nmt.edu
+
+[TEST]
+ftp_dir = AUTO/TEST
+ftp_reconnect_wait = 5
+mock_test = True
+testmode = False
+ftp_timeout = 5
+
+[FILES]
+logfile = data2passcal.log
+
diff --git a/data2passcal/config.py b/data2passcal/config.py
new file mode 100644
index 0000000000000000000000000000000000000000..6f3ac698871c815665f4efe475f7e8570bc08de4
--- /dev/null
+++ b/data2passcal/config.py
@@ -0,0 +1,31 @@
+#!/usr/bin/env python
+'''
+data2passcal
+
+Create configuration file
+'''
+
+import configparser
+
+VERSION = '2020.107'
+
+config = configparser.ConfigParser()
+config['FTP_INFO'] = {'FTP_IP': '129.138.26.29',
+                      'FTP_HOST': 'qc.passcal.nmt.edu',
+                      'FTP_USER': 'ftp',
+                      'FTP_PASSWORD': 'data2passcal',
+                      'FTP_DIR': 'AUTO/MSEED',
+                      'FTP_TIMEOUT': '120',
+                      'FTP_RECONNECT_WAIT': '60',
+                      'FTP_BLOCKSIZE': '8192',
+                      'FTP_SEND_ATTEMPTS': '3',
+                      'FTP_DEBUG_LEVEL': '0'}
+config['TEST'] = {'TESTMODE': 'False',
+                  'FTP_DIR': 'AUTO/TEST',
+                  'FTP_TIMEOUT': '5',
+                  'FTP_RECONNECT_WAIT': '5',
+                  'MOCK_TEST': 'True'}
+config['FILES'] = {'LOGFILE': 'data2passcal.log'}
+
+with open('config.ini', 'w') as configfile:
+    config.write(configfile)
diff --git a/data2passcal/data2passcal.py b/data2passcal/data2passcal.py
index a4afaafc0317720dab90d5c265387f05d369a197..7cb855196ee6f11a189b0478270d8815548fbd6d 100644
--- a/data2passcal/data2passcal.py
+++ b/data2passcal/data2passcal.py
@@ -5,62 +5,77 @@ data2passcal
 Ship miniseed data to passcal via ftp for qc before archival at DMC DMS
 Lloyd Carothers IRIS/PASSCAL
 '''
-from __future__ import print_function, division
-from builtins import *
-VERSION = '2020.093'
+from __future__ import division, print_function
 
-import ftplib
-import sys, os, signal, struct, pickle, logging
-from time import time, sleep
+import configparser
 import datetime
+import ftplib
+import logging
+import os
+import pickle
+import re
+import signal
+import struct
+import sys
+from time import sleep, time
 
-#TESTMODE = True
-TESTMODE = False
-# Verbosity of console log from logging module from DEBUG to CRITICAL
-DEBUG =  logging.DEBUG
-# After transmission or on interrupt try to ftp logfile to PIC
-#SEND_LOGFILE = True
-
-#FTP related
-FTP_IP = '129.138.26.29'
-FTP_HOST = 'qc.passcal.nmt.edu'
-FTP_USER = 'ftp'
-FTP_PASSWORD = 'data2passcal'
-FTP_DIR = 'AUTO/MSEED'
-FTP_TIMEOUT = 120
-FTP_RECONNECT_WAIT = 60
+VERSION = '2020.107'
+
+
+# Cache the ftplib.FTP class so it will be available to test_FTP(FTP) when calling isinstance assert
+FTPCache = ftplib.FTP
+
+# Parse configuration file
+config = configparser.ConfigParser()
+print(config.read('data2passcal/config.ini'))
+
+
+# TEST related
+TESTMODE = config.getboolean('TEST', 'TESTMODE')
+
+# FTP related
+FTP_IP = config.get('FTP_INFO', 'FTP_IP')
+FTP_HOST = config.get('FTP_INFO', 'FTP_HOST')
+FTP_USER = config.get('FTP_INFO', 'FTP_USER')
+FTP_PASSWORD = config.get('FTP_INFO', 'FTP_PASSWORD')
+FTP_DIR = config.get('FTP_INFO', 'FTP_DIR')
+FTP_TIMEOUT = config.getint('FTP_INFO', 'FTP_TIMEOUT')
+FTP_RECONNECT_WAIT = config.getint('FTP_INFO', 'FTP_RECONNECT_WAIT')
 if TESTMODE:
-    FTP_DIR = 'AUTO/TEST'
-    FTP_TIMEOUT = 5
-    FTP_RECONNECT_WAIT = 5
-FTP_BLOCKSIZE = 8192
-#number of time to try to open the ftp connection
-#FTP_CONNECT_ATTEMPTS = 3
-# testing will retry for a week. Why not?
-FTP_CONNECT_ATTEMPTS = 60*60*24*7  / FTP_RECONNECT_WAIT
-#number of times a single file with try to be sent
-FTP_SEND_ATTEMPTS = 3
+    FTP_DIR = config.get('TEST', 'FTP_DIR')
+    FTP_TIMEOUT = config.getint('TEST', 'FTP_TIMEOUT')
+    FTP_RECONNECT_WAIT = config.getint('TEST', 'FTP_RECONNECT_WAIT')
+FTP_BLOCKSIZE = config.getint('FTP_INFO', 'FTP_BLOCKSIZE')
+# number of time to try to open the ftp connection - testing will retry for a week. Why not?
+FTP_CONNECT_ATTEMPTS = 60 * 60 * 24 * 7 / FTP_RECONNECT_WAIT
+# number of times a single file with try to be sent
+FTP_SEND_ATTEMPTS = config.getint('FTP_INFO', 'FTP_SEND_ATTEMPTS')
 # debug level of ftplib, 0-3
-FTP_DEBUG_LEVEL = 0
-#store the files sent in ~/data2passcal.sent
-SENTFILE = os.path.join( os.path.expanduser('~'), '.data2passcal.sent')
+FTP_DEBUG_LEVEL = config.getint('FTP_INFO', 'FTP_DEBUG_LEVEL')
+
+# Files related
+LOGFILE = config.get('FILES', 'LOGFILE')
+# store the files sent in ~/data2passcal.sent
+SENTFILE = os.path.join(os.path.expanduser('~'), '.data2passcal.sent')
 # If this file exists open it, incorporate, and save to new name, and delete old
-SENTFILE_OLD = os.path.join( os.path.expanduser('~'), '.send2passcal.sent')
-LOGFILE = 'data2passcal.log'
+SENTFILE_OLD = os.path.join(os.path.expanduser('~'), '.send2passcal.sent')
 
 HELP = '''
 data2passcal
 VERSION: %s
 Usage: data2passcal dir
-  data2passcal is a utility that sends MSEED files ready for archival at the DMC to PASSCAL's QC system by:
-  Scans all files below directory dir.
-  Filters out non-miniseed files, by inspecting the first blockette of each file.
-  Sends the files to the automated system for ingestion into the QC system via ftp.
+  data2passcal is a utility that sends day-long MSEED files ready for archival at the DMC to PASSCAL's QC system by:
+  Scanning all files below directory dir.
+  Filtering out non-miniseed files, by inspecting the first blockette of each file.
+  Sending the files to the automated system for ingestion into the QC system via ftp.
   You can send a SIGTERM (ctl-c) to data2passcal and it will shutdown cleanly.
   A list of sent files is kept in ~/.data2passcal.sent. Subsequent runs of send2passcal will not send files already sent.
   A log is stored in data2passcal.log in the current directory.
 ''' % VERSION
 
+# Verbosity of console log from logging module from DEBUG to CRITICAL
+DEBUG = logging.DEBUG
+
 # Configure logging file and stdout
 # todo if sending a file do we want to send all previous logs even if already sent
 logger = logging.getLogger('__name__')
@@ -73,16 +88,17 @@ logger.addHandler(logfh)
 # Log to stdout
 logconsole = logging.StreamHandler()
 logconsole.setLevel(logging.INFO)
-logconsole.setFormatter(logging.Formatter('%(message)s' ))
+logconsole.setFormatter(logging.Formatter('%(message)s'))
 logger.addHandler(logconsole)
 logger.debug('Program starting.')
-logger.debug('%s - v%s - Python:%s' % (sys.argv, VERSION, sys.version) )
+logger.debug('%s - v%s - Python:%s' % (sys.argv, VERSION, sys.version))
 logger.debug('CWD: %s' % os.getcwd())
-logger.info('Version: '+ VERSION)
+logger.info('Version: ' + VERSION)
 logger.info('TIMEOUT: %d' % FTP_TIMEOUT)
 logger.info('FTP RECONNECT WAIT: %d' % FTP_RECONNECT_WAIT)
 # -- End logging
 
+
 def scan_dir(dir):
     '''Returns a list of absolute file names found below root dir'''
     rootdir = dir
@@ -99,17 +115,18 @@ def scan_dir(dir):
             try:
                 filesize += os.path.getsize(f)
             except OSError:
-                logger.debug('Can not stat %s'% f)
+                logger.debug('Can not stat %s' % f)
             else:
                 filelist.append(f)
-    logger.info('Total Size = %s'% format_size(filesize) )
-    logger.info('Total Files = %s'% len(filelist) )
-    logger.info('Total Dirs = %s'% foldercount)
-    logger.info('Scan time = %0.2fs'% (time() - starttime))
+    logger.info('Total Size = %s' % format_size(filesize))
+    logger.info('Total Files = %s' % len(filelist))
+    logger.info('Total Dirs = %s' % foldercount)
+    logger.info('Scan time = %0.2fs' % (time() - starttime))
     print()
 
     return filelist
 
+
 def sendable(file):
     '''Filters files scanned returning a new list of files to send i.e. miniseed'''
     if os.path.basename(file).startswith('.'):
@@ -118,11 +135,12 @@ def sendable(file):
         return True
     return False
 
-#Basic Miniseed file metadata extracted from filename
-import re
-#This includes .p files which we may accept in the future but won't h
+
+# Basic Miniseed file metadata extracted from filename
+# This includes .p files which we may accept in the future but won't h
 MseedRE = re.compile(r'\A(.*)\.([A-Z0-9][A-Z0-9])\.(.*)\.([A-Z][A-Z]\w)\.([0-9]{4})\.([0-9]{3})(?:\.p)*')
 
+
 def filename_qc_format(file):
     '''Used to filter filename if correct for qc system'''
     return MseedRE.match(os.path.basename(file))
@@ -130,15 +148,16 @@ def filename_qc_format(file):
 
 def format_size(num):
     '''Format bytes into human readble with suffix'''
-    for suffix in [ 'bytes', 'KB', 'MB', 'GB'  ]:
+    for suffix in ['bytes', 'KB', 'MB', 'GB']:
         if num < 1024.0:
             return '%3.3f %s' % (num, suffix)
         num /= 1024.0
     return '%3.3f %s' % (num, 'TB')
 
+
 def ismseed(file):
     try:
-        ms = open(file,'rb')
+        ms = open(file, 'rb')
     except:
         return None
     order = ByteOrder(ms)
@@ -150,24 +169,26 @@ def ismseed(file):
         return False
 
 #########################################################
-#needs import sys,struct
-#Taken from Bruces LibTrace
-def ByteOrder(infile, seekval=20) :
+# needs import sys,struct
+# Taken from Bruces LibTrace
+
+
+def ByteOrder(infile, seekval=20):
     """
     read file as if it is mseed just pulling time info
     from fixed header and determine if it makes sense unpacked
     as big endian or little endian
     """
     Order = "unknown"
-    try :
-        #seek to timeblock and read
+    try:
+        # seek to timeblock and read
         infile.seek(seekval)
-        timeblock=infile.read(10)
+        timeblock = infile.read(10)
 
-        #assume big endian
-        (Year, Day, Hour, Min, Sec, junk, Micro)=\
-         struct.unpack('>HHBBBBH',timeblock)
-        #test if big endian read makes sense
+        # assume big endian
+        (Year, Day, Hour, Min, Sec, junk, Micro) =\
+            struct.unpack('>HHBBBBH', timeblock)
+        # test if big endian read makes sense
         if 1950 <= Year <= 2050 and \
            1 <= Day <= 366 and \
            0 <= Hour <= 23 and \
@@ -175,23 +196,24 @@ def ByteOrder(infile, seekval=20) :
            0 <= Sec <= 59:
             Order = "big"
         else:
-            #try little endian read
-            (Year, Day, Hour, Min, Sec, junk, Micro)=\
-             struct.unpack('<HHBBBBH',timeblock)
-            #test if little endian read makes sense
+            # try little endian read
+            (Year, Day, Hour, Min, Sec, junk, Micro) =\
+                struct.unpack('<HHBBBBH', timeblock)
+            # test if little endian read makes sense
             if 1950 <= Year <= 2050 and \
                1 <= Day <= 366 and \
                0 <= Hour <= 23 and \
                0 <= Min <= 59 and \
                0 <= Sec <= 59:
                 Order = "little"
-    except Exception as e:
+    except Exception:
         pass
 
     return Order
 #########################################################
 
-def get_sent_file_list(sentfile = SENTFILE):
+
+def get_sent_file_list(sentfile=SENTFILE):
     sentlist = []
     if os.path.isfile(sentfile):
         logger.debug('Using sentfile %s' % sentfile)
@@ -205,7 +227,8 @@ def get_sent_file_list(sentfile = SENTFILE):
                 sentlist = pickle.load(f)
     return sentlist
 
-def write_sent_file_list(sentlist , sentfile = SENTFILE ):
+
+def write_sent_file_list(sentlist, sentfile=SENTFILE):
     logger.info('Saving list of files sent to passcal')
     with open(sentfile, 'wb+') as f:
         pickle.dump(sentlist, f, protocol=2)
@@ -218,8 +241,8 @@ def get_FTP():
         trys += 1
         try:
             import socket
-            logger.info('Connecting to FTP host %s from %s. Attempt %d of %d' % ( FTP_HOST, socket.gethostbyname(socket.gethostname()), trys, FTP_CONNECT_ATTEMPTS))
-            FTP =  ftplib.FTP(host=FTP_HOST, user=FTP_USER, passwd=FTP_PASSWORD, timeout=FTP_TIMEOUT)
+            logger.info('Connecting to FTP host %s from %s. Attempt %d of %d' % (FTP_HOST, socket.gethostbyname(socket.gethostname()), trys, FTP_CONNECT_ATTEMPTS))
+            FTP = ftplib.FTP(host=FTP_HOST, user=FTP_USER, passwd=FTP_PASSWORD, timeout=FTP_TIMEOUT)
             FTP.set_debuglevel(FTP_DEBUG_LEVEL)
             FTP.cwd(FTP_DIR)
             FTP.set_pasv(True)
@@ -235,29 +258,36 @@ def get_FTP():
     logger.error('Giving up.')
     return None
 
+
 def test_network():
     passcal_http_reachable()
     google_http_reachable()
     passcal_ftp_reachable()
 
+
 def passcal_http_reachable():
     '''download and time passcal home page'''
     url = 'http://www.passcal.nmt.edu/'
     return url_reachable(url=url)
 
+
 def google_http_reachable():
     '''download and time passcal home page'''
     url = 'http://www.google.com/'
     return url_reachable(url=url)
 
+
 def passcal_ftp_reachable():
     '''Download a small file from passcals general ftp'''
     url = 'ftp://ftp.passcal.nmt.edu/download/public/test.dat'
     return url_reachable(url=url)
 
-def url_reachable(url = 'http://www.passcal.nmt.edu/'):
+
+def url_reachable(url='http://www.passcal.nmt.edu/'):
     '''fetches a url returns True or False'''
-    import urllib.request, urllib.error, urllib.parse
+    import urllib.request
+    import urllib.error
+    import urllib.parse
     start = time()
     try:
         f = urllib.request.urlopen(url)
@@ -265,20 +295,20 @@ def url_reachable(url = 'http://www.passcal.nmt.edu/'):
         logger.error("Failed to open connection to %s" % url)
         logger.error(e)
         return False
-    logger.info('connection made to %s in %f sec' % ( url, time() - start) )
+    logger.info('connection made to %s in %f sec' % (url, time() - start))
     data = f.read()
     runtime = time() - start
     size = len(data)
-    rate = size/runtime
-    logger.info('%d B/sec in %f sec' % (rate, runtime) )
+    rate = size / runtime
+    logger.info('%d B/sec in %f sec' % (rate, runtime))
     return True
 
 
 def test_FTP(FTP):
     try:
-        assert isinstance(FTP, ftplib.FTP)
+        assert isinstance(FTP, FTPCache)
         FTP.voidcmd('NOOP')
-    except ftplib.all_errors as e :
+    except ftplib.all_errors as e:
         logger.error(e)
         return False
     except AssertionError as e:
@@ -294,11 +324,11 @@ def send2passcal(mslist, sentlist=None):
     # Handle SIGINT while in this function: to gracefully close connection and save sentlist to disk file
     def signal_handler(signum, frame):
         print()
-        logger.info('Caught interrupt while FTPing. Aborting transfer %s' % current_file )
-        logger.info('Sent %d of %d' % (num_sent, num_to_send) )
-        logger.info('Sent %s of %s' % (format_size(size_sent), format_size(size_to_send)) )
-        logger.info('%s /sec' % ( format_size(size_sent / (time() - starttime )) ) )
-        logger.info('Ran for %f sec' % (time()-starttime) )
+        logger.info('Caught interrupt while FTPing. Aborting transfer %s' % current_file)
+        logger.info('Sent %d of %d' % (num_sent, num_to_send))
+        logger.info('Sent %s of %s' % (format_size(size_sent), format_size(size_to_send)))
+        logger.info('%s /sec' % (format_size(size_sent / (time() - starttime))))
+        logger.info('Ran for %f sec' % (time() - starttime))
         write_sent_file_list(sentlist)
         try:
             if FTP:
@@ -312,7 +342,7 @@ def send2passcal(mslist, sentlist=None):
         '''Updates the terminal display.'''
         signal.signal(signal.SIGINT, signal_handler)
         update.bytes_sent += len(data)
-        print('\r' + str(PB) + ' %s /sec ' % ( format_size(size_sent / (time() - starttime )) ), end=' ')
+        print('\r' + str(PB) + ' %s /sec ' % (format_size(size_sent / (time() - starttime))), end=' ')
         '''
         print '%s %0.2f%%. %0.10d / %0.10d.' % ( current_file.center(20),
                                                 (update.bytes_sent / update.file_size)*100,
@@ -320,8 +350,8 @@ def send2passcal(mslist, sentlist=None):
                                                 file_size,
                                                 ) ,
         '''
-        ETA_sec = ((time() - starttime) / size_sent ) * (size_to_send - size_sent)
-        print('ETA %s %s %s' % (  str(datetime.timedelta(seconds=(ETA_sec))), current_file.center(20), ' '*20 ), end=' ')
+        ETA_sec = ((time() - starttime) / size_sent) * (size_to_send - size_sent)
+        print('ETA %s %s %s' % (str(datetime.timedelta(seconds=(ETA_sec))), current_file.center(20), ' ' * 20), end=' ')
         sys.stdout.flush()
 
     if sentlist is None:
@@ -334,7 +364,7 @@ def send2passcal(mslist, sentlist=None):
     size_sent = 1
     current_file = ''
     signal.signal(signal.SIGINT, signal_handler)
-    logger.info('Sending %d, %s files to PASSCAL' % (num_to_send, format_size (size_to_send)))
+    logger.info('Sending %d, %s files to PASSCAL' % (num_to_send, format_size(size_to_send)))
     FTP = get_FTP()
     starttime = time()
     PB = ProgressBar(num_to_send)
@@ -349,19 +379,19 @@ def send2passcal(mslist, sentlist=None):
                 fh = open(f, 'rb')
                 file_size = os.path.getsize(f)
                 update.file_size = float(file_size)
-                FTP.storbinary('STOR %s' % current_file , fh, blocksize=FTP_BLOCKSIZE, callback=update)
+                FTP.storbinary('STOR %s' % current_file, fh, blocksize=FTP_BLOCKSIZE, callback=update)
             except ftplib.error_perm as e:
                 # This is permission and the error when 550 for the .in file already exists so we should just continue with the next file
                 # todo create a list of failed files and resend those at the end instead of requiring a rerun
-                logger.error( 'Failed to send file %s, permission error. Skipping...' % current_file)
+                logger.error('Failed to send file %s, permission error. Skipping...' % current_file)
                 logger.error(e)
                 break
-            except (ftplib.all_errors, AttributeError) as e :
-                #since we can restore with how we have proftp setup. There is nothing more we can do with this file
-                #Until the server rms the .in.file
-                logger.error('Failed to send file %s.' % ( current_file) ) #, trys, FTP_SEND_ATTEMPTS)
+            except (ftplib.all_errors, AttributeError) as e:
+                # since we can restore with how we have proftp setup. There is nothing more we can do with this file
+                # Until the server rms the .in.file
+                logger.error('Failed to send file %s.' % (current_file))  # , trys, FTP_SEND_ATTEMPTS)
                 logger.error(e)
-                #if DEBUG: print "Waiting %d..." % FTP_TIMEOUT; sleep(FTP_TIMEOUT)
+                # if DEBUG: print "Waiting %d..." % FTP_TIMEOUT; sleep(FTP_TIMEOUT)
                 try:
                     if FTP:
                         FTP.abort()
@@ -380,15 +410,17 @@ def send2passcal(mslist, sentlist=None):
                 break
 
     print()
-    logger.info( 'Sent %d of %d' % (num_sent, num_to_send) )
-    logger.info( 'Sent %s of %s' % (format_size(size_sent), format_size(size_to_send)) )
-    logger.info( '%s /sec' % ( format_size(size_sent / (time() - starttime )) ) )
-    logger.info( 'Ran for %f sec' % (time()-starttime) )
+    logger.info('Sent %d of %d' % (num_sent, num_to_send))
+    logger.info('Sent %s of %s' % (format_size(size_sent), format_size(size_to_send)))
+    logger.info('%s /sec' % (format_size(size_sent / (time() - starttime))))
+    logger.info('Ran for %f sec' % (time() - starttime))
     if FTP and test_FTP(FTP):
         FTP.quit()
 
 ########################################################
 # From progressbar 3rd party class will eventually dump not awesome
+
+
 class ProgressBar(object):
     def __init__(self, duration):
         self.duration = duration
@@ -409,7 +441,7 @@ class ProgressBar(object):
 
     def update_time(self, elapsed_secs):
         self.__update_amount((elapsed_secs / float(self.duration)) * 100.0)
-        #self.prog_bar += '  %d/%s files' % (elapsed_secs, self.duration)
+        # self.prog_bar += '  %d/%s files' % (elapsed_secs, self.duration)
 
     def __update_amount(self, new_amount):
         percent_done = int(round((new_amount / 100.0) * 100.0))
@@ -426,8 +458,10 @@ class ProgressBar(object):
 
 # End progressbar
 ########################################################
+
+
 def main():
-    if len(sys.argv) < 2 or sys.argv[1] in [ '-h', '--help', '-?']:
+    if len(sys.argv) < 2 or sys.argv[1] in ['-h', '--help', '-?']:
         print(HELP)
         sys.exit()
     # find all files below dir
@@ -436,19 +470,20 @@ def main():
     logger.info('Removing improperly named files.')
     msnamedlist = list(filter(filename_qc_format, filelist))
     logger.info('Properly named files files: %d' % len(msnamedlist))
-    logger.info('Other files: %d'% (len(filelist) - len(msnamedlist)))
+    logger.info('Other files: %d' % (len(filelist) - len(msnamedlist)))
     print()
     logger.info('Removing files that are not Miniseed.')
     mslist = list(filter(sendable, msnamedlist))
     logger.info('MiniSEED files: %d' % len(mslist))
-    logger.info('Properly named but not miniseed files: %d'% (len(msnamedlist) - len(mslist)))
+    logger.info('Properly named but not miniseed files: %d' % (len(msnamedlist) - len(mslist)))
     print()
     logger.info('Removing files already sent to PASSCAL')
     sentlist = get_sent_file_list()
     unsentms = [f for f in mslist if f not in sentlist]
-    logger.info('%d miniSEED files have already been sent, not resending.' % (len( mslist) - len(unsentms)))
+    logger.info('%d miniSEED files have already been sent, not resending.' % (len(mslist) - len(unsentms)))
     send2passcal(unsentms, sentlist)
     write_sent_file_list(sentlist)
 
+
 if __name__ == '__main__':
     main()
diff --git a/setup.cfg b/setup.cfg
index ac124ca411cea8f9dabc9e8c0af2752987f18625..a6afa20c057127edbc8738bc2805b6bb1c8c5fc7 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,5 +1,5 @@
 [bumpversion]
-current_version = 2020.093
+current_version = 2020.107
 commit = True
 tag = True
 
diff --git a/setup.py b/setup.py
index 6afb3d2ddf200dcb6435ce2c75f77154b81a83e3..9f29d0f7b3af75df852fe0cc018c63e905075029 100644
--- a/setup.py
+++ b/setup.py
@@ -29,8 +29,11 @@ setup(
             'data2passcal=data2passcal.data2passcal:main',
         ],
     },
-    install_requires=[],
-    setup_requires = [],
+    install_requires=[
+        "mock;python_version<'3.3'",
+        "configparser;python_version<'3.2'"
+    ],
+    setup_requires=[],
     extras_require={
         'dev': [
             'pip',
@@ -42,6 +45,7 @@ setup(
             'coverage',
             'Sphinx',
             'twine',
+            'pytest'
         ]
     },
     license="GNU General Public License v3",
@@ -52,6 +56,6 @@ setup(
     packages=find_packages(include=['data2passcal']),
     test_suite='tests',
     url='https://git.passcal.nmt.edu/passoft/data2passcal',
-    version='2020.093',
+    version='2020.107',
     zip_safe=False,
 )
diff --git a/tests/test_data/ST00.AB..BHZ.2007.160 b/tests/test_data/ST00.AB..BHZ.2007.160
new file mode 100644
index 0000000000000000000000000000000000000000..668ccee282506f7101f4f6de9e5175a3103d0043
Binary files /dev/null and b/tests/test_data/ST00.AB..BHZ.2007.160 differ
diff --git a/tests/test_data/ST00.AB..BHZ.2007.161 b/tests/test_data/ST00.AB..BHZ.2007.161
new file mode 100644
index 0000000000000000000000000000000000000000..f3fa1ece76f3655d59e123b5d92c235b4f5e9e4a
Binary files /dev/null and b/tests/test_data/ST00.AB..BHZ.2007.161 differ
diff --git a/tests/test_data/ST00.AB..BHZ.2007.162 b/tests/test_data/ST00.AB..BHZ.2007.162
new file mode 100644
index 0000000000000000000000000000000000000000..75c29514caed9cc89925cc00f5edca42c11a3950
Binary files /dev/null and b/tests/test_data/ST00.AB..BHZ.2007.162 differ
diff --git a/tests/test_data/ST00.AB..BHZ.2007.163 b/tests/test_data/ST00.AB..BHZ.2007.163
new file mode 100644
index 0000000000000000000000000000000000000000..907b07ca7097a791aa3c346e26df10f9c7788851
Binary files /dev/null and b/tests/test_data/ST00.AB..BHZ.2007.163 differ
diff --git a/tests/test_data/ST00.AB..BHZ.2007.164 b/tests/test_data/ST00.AB..BHZ.2007.164
new file mode 100644
index 0000000000000000000000000000000000000000..b925f35dace4e038f204d9b993113d9318f277e5
Binary files /dev/null and b/tests/test_data/ST00.AB..BHZ.2007.164 differ
diff --git a/tests/test_data2passcal.py b/tests/test_data2passcal.py
index a82fa89fd59939d3dfaf33a9cf4ba03f1c71c670..02d5c3319849c78c498560163d2000e8f4fd3a57 100644
--- a/tests/test_data2passcal.py
+++ b/tests/test_data2passcal.py
@@ -3,25 +3,102 @@
 
 """Tests for `data2passcal` package."""
 
+from __future__ import division, print_function
+
+import configparser
+import ftplib
+import os
 import unittest
-import sys
+
+from data2passcal.data2passcal import get_FTP, ismseed, scan_dir, send2passcal
+
+VERSION = '2020.107'
+
 
 try:
-    import data2passcal
+    from unittest.mock import patch
 except ImportError:
-     pass
+    from mock import patch
+
+config = configparser.ConfigParser()
+config.read('data2passcal/config.ini')
+FTP_HOST = config.get('FTP_INFO', 'FTP_HOST')
+FTP_USER = config.get('FTP_INFO', 'FTP_USER')
+FTP_PASSWORD = config.get('FTP_INFO', 'FTP_PASSWORD')
+FTP_DIR = config.get('FTP_INFO', 'FTP_DIR')
+FTP_TIMEOUT = config.getint('FTP_INFO', 'FTP_TIMEOUT')
+FTP_RECONNECT_WAIT = config.getint('FTP_INFO', 'FTP_RECONNECT_WAIT')
+FTP_CONNECT_ATTEMPTS = 60 * 60 * 24 * 7 / FTP_RECONNECT_WAIT
+FTP_SEND_ATTEMPTS = config.getint('FTP_INFO', 'FTP_SEND_ATTEMPTS')
+MOCK_TEST = config.getboolean('TEST', 'MOCK_TEST')
+
 
 class TestData2passcal(unittest.TestCase):
     """Tests for `data2passcal` package."""
 
     def setUp(self):
-        """Set up test fixtures, if any."""
+        """Set up test fixtures, if any"""
+        dir_testdata = os.path.dirname(os.path.realpath(__file__)) + '/test_data'
+        filelist = [x for x in scan_dir(dir_testdata) if not os.path.basename(x).startswith('.') and not os.path.basename(x).endswith('.log')]
+        self.dir_testdata = dir_testdata
+        self.filelist = filelist
+
+    def test_scan_dir(self):
+        """
+        Test basic functionality of scan_dir function
+        Only 5 files available under ./test_data directory
+        Not taking into account .DS_STORE file
+        """
+        self.assertEqual(len(self.filelist), 5, 'Incorrect number of files')
+
+    def test_ismseed(self):
+        """Test basic functionality of ismseed function"""
+        for f in self.filelist:
+            self.assertTrue(ismseed(f), '{} is not a miniseed file'.format(os.path.basename(f)))
+
+    @patch('data2passcal.data2passcal.ftplib.FTP', autospec=True)
+    def test_get_FTP_mock(self, mock_ftp_constructor):
+        """Mock test creating ftp connection to PASSCAL"""
+        mock_ftp = mock_ftp_constructor.return_value
+        get_FTP()
+        mock_ftp_constructor.assert_called_with(host=FTP_HOST, user=FTP_USER, passwd=FTP_PASSWORD, timeout=FTP_TIMEOUT)
+        mock_ftp.cwd.assert_called_with(FTP_DIR)
+        self.assertLess(mock_ftp_constructor.call_count, FTP_CONNECT_ATTEMPTS, 'Number of ftp connection attempts exceeeds {}'.format(FTP_CONNECT_ATTEMPTS))
+        mock_ftp.quit()
+
+    @patch('data2passcal.data2passcal.ftplib.FTP', autospec=True)
+    def test_send_data_mock(self, mock_ftp_constructor):
+        """Mock test sending MSEED files (test data) to PASSCAL's QC system"""
+        mock_ftp = mock_ftp_constructor.return_value
+        send2passcal(self.filelist)
+        self.assertTrue(mock_ftp.storbinary.called, 'No data sent')
+        self.assertEqual(mock_ftp.storbinary.call_count, len(self.filelist), 'Failed to send all files - Sent {0} of {1}'.format(mock_ftp.storbinary.call_count, len(self.filelist)))
+        files_sent = []
+        for x in mock_ftp.storbinary.call_args_list:
+            args, kwargs = x
+            files_sent.append(args[0].split(' ')[1])
+        for f in self.filelist:
+            f = os.path.basename(f)
+            self.assertLess(files_sent.count(f), FTP_SEND_ATTEMPTS, 'Attempted to send file {0} more than {1} times'.format(f, FTP_SEND_ATTEMPTS))
+
+    @unittest.skipIf(MOCK_TEST == True, "skipping real send2passcal test")
+    def test_send_data(self):
+        """Test sending MSEED files (test data) to PASSCAL's QC system - Optional"""
+        ftp = get_FTP()
+        send2passcal(self.filelist)
+        wdir = ftp.pwd()
+        try:
+            files_sent = [os.path.basename(x) for x in ftp.nlst(wdir)]
+        except ftplib.error_perm() as resp:
+            if str(resp) == "550 No files found":
+                print("No files found in this directory")
+            else:
+                raise
+        for f in self.filelist:
+            f_ = os.path.basename(f)
+            self.assertIn(f_, files_sent, 'File {} was not sent to PASSCAL'.format(f_))
+        ftp.quit()
 
-    def tearDown(self):
-        """Tear down test fixtures, if any."""
 
-    def test_import(self):
-        if 'data2passcal' in sys.modules:
-            self.assertTrue(True, "data2passcal loaded")
-        else:
-            self.fail()
+if __name__ == '__main__':
+    unittest.main()
diff --git a/tox.ini b/tox.ini
index 60d2b1bb52fdc8539b7f39176385af7599226b5a..f6954a3b222f97b8819393542d29ed2b6ff99bc7 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,5 +1,5 @@
 [tox]
-envlist = py27, py36 flake8
+envlist = py27, py36, flake8
 
 [travis]
 python =
@@ -9,11 +9,10 @@ python =
 [testenv:flake8]
 basepython = python
 deps = flake8
-commands = flake8 data2passcal
+commands = flake8 --ignore=E722,E712,E501 data2passcal
+           flake8 --ignore=E722,E712,E501 tests
 
 [testenv]
-setenv =
-    PYTHONPATH = {toxinidir}
-commands=python setup.py test
-
-
+deps = pytest
+setenv = PYTHONPATH = {toxinidir}
+commands = pytest