# -*- coding: utf-8 -*- ======= CHANGES ======= Release 0.15.0 (2014-07-01) --------------------------- * [enhance] oktest.web.WSGITest class supports multipart form data. ## build multipart data from oktest.web import MultiPart mp = MultiPart() # or boundary='abcdef'; mp = MutliPart(boundary) mp.add("name1", "value1") # add string value with open("logo.png", 'wb') as f: # add file value mp.add("file1", f.read(), "logo.png", "image/png") ## test with multipart data from oktest.web import WSGITest http = WSGITest(wsgi_app) resp = http.POST('/upload', multipart=mp) # or params=mp ok (resp).is_response(200) * [enhance] oktest.web.WSGITest class supports 'Cookie' and 'Set-Cookie'. from oktest.web import WSGITest http = WSGITest(wsgi_app) ## request: 'Cookie' header resp = http.GET('/', cookies='name=value') # or cookies={'name':'value'} ## response: 'Set-Cookie' header ok (resp).is_response(200).cookie('name', 'value') ok (resp).is_response(200).cookie('name', re.compile(r'^value$'), domain='www.example.com', path='/cgi' expires='Wed, 01-Jan-2020 12:34:56 GMT', max_age='1200', secure=True, httponly=True) * [enhance] New assertion methods. ok (xs).all(lambda x: x is None) # ok when all items in xs are None ok (xs).any(lambda x: x is None) # ok when there is None in xs ok (x).between(minval, maxval) # ok when minval <= x <= maxval ok (xs).length([minlen, maxlen]) # ok when minlen <= len(xs) <= maxlen ok (dictionary).has_key('key') # ok when dictinary has key ok (dictionary).has_item('key','val') # ok when dictionary has key an val * [enhance] New utility function 'options_of()' to get user-defined options. ex: import unittest from oktest import ok, test, options_of class FooTest(unittest.TestCase): def setUp(self): ## change setUp() behaviour according to tag dictionary = options_of(self) if dictionary.get('tag') == "experimental": .... @test("example", tag="experimental", num=123) def _(self): assert options_of(self) == {'tag': "experimental", 'num': 123} * [bugfix] oktest.web.WSGITest now works on Python 3.4. * [bugfix] fix oktest.web.WSGITest class to encode urlpath when multibyte. Release 0.14.2 (2014-03-20) --------------------------- * [bugfix] Avoids unicode error on environments that sys.stdout.encoding is 'ISO-8859-1' or 'ANSI_X3.4-1968'. Release 0.14.1 (2014-03-11) --------------------------- * [enhance] WSGITest class supports `params' parameter. Example:: from oktest.web import WSGITest http = WSGITest(app) resp = http.GET('/', params={'x':'1'}) # same as http.GET('/', query={'x':'1'}) resp = http.POST('/', params={'x':'1'}) # same as http.GET('/', form={'x':'1'}) Release 0.14.0 (2014-03-10) --------------------------- * [enhance] Response object returned by `WSGITest#GET()' or '#POST()' now supports `body_json' property. Example:: from oktest.web import WSGITest http = WSGITest(app) resp = http.GET('/') print(resp.body_json) * [change] `headers` argument of `WSGITest#GET()' (or '#POST()' and so on) now means HTTP headers, not environ values. Example:: ## version <= 0.13 http.GET('/', headers={'HTTP_COOKIE': 'name=val'}) ## version >= 0.14 http.GET('/', headers={'Cookie': 'name=val'}) ## or http.GET('/', environ={'HTTP_COOKIE': 'name=val'}) * [enhance] (Experimental) `oktest.validator.Validator' class is added. It is convenient to test complex data structure. Example:: from oktest.validator import Validator as V ok (resp.body_json) == { "status": "OK", "member": { "name": "Haruhi", "gender": V('gender', enum=('F', 'M')), "age": V('age', type=int, between=(15, 18)), "birthday": V('created_at', pattern=r'^\d\d\d\d-\d\d-\d\d$') } } See users guide for details. http://www.kuwata-lab.com/oktest/oktest-py_users-guide.html#validator Release 0.13.0 (2014-01-23) --------------------------- * [enhance] `ok().is_response()' now supports Requests. Example:: import requests resp = requests.get('http://www.example.com/') ok (resp).is_response(200, 'text/html') * [enhance] (Experimental) Add 'oktest.web' module to help WSGI app testing. Example:: ## create WSGI application class App(object): def __call__(self, environ, start_response): status = '200 OK' headers = [('Content-Type', 'application/json')] body = [b'''{"message":"Hello!"}'''] # bytes, not unicode start_response(status, headers) return body app = App() ## test for app import unittest import oktest from oktest import test, ok, subject from oktest.web import WSGITest # !!!!! http = WSGITest(app) # !!!!! https = WSGITest(app, {'HTTPS': 'on'}) # !!!!! class AppTest(unittest.TestCase): with subject('GET /'): @test("Returns messaging JSON.") def _(self): resp = http.GET('/') # or http('GET', '/') ok (resp).is_response(200).json({"message": "Hello!"}) ## or status, headers, body = http.GET('/') # or http('GET', '/') ok (status) == '200 OK' ok (headers) == [('Content-Type', 'application/json')] ok (body) == [b'''{"message":"Hello!"}'''] if __name__ == '__main__': oktest.main() Release 0.12.1 (2014-01-20) --------------------------- * [bugfix] `ok()' object now doesn't have http response assertions. You must call `ok().is_response()' at first to assert response object. :: hasattr(ok(response).is_response(), 'json') # True hasattr(ok(response), 'json') # True on 0.12.0, False on 0.12.1 * [bugfix] `ok().should' now allows method chain. ex:: ok ("image001.jpg").should.startswith('image').endswith(('.jpg', '.png')) Release 0.12.0 (2014-01-12) --------------------------- * [enhance] `ok (actual) == expected' now prints difference between actual and expected values with `pprint.pformat()' when values are one of list, tuple, or dict. ex:: class ExampleTest(unittest.TestCase): def test_ex1(self): expected = { 'username': "Haruhi", 'gender': "Female", 'email': "haruhi@sos-brigade.org", } actual = { 'username': "Haruhi", 'gender': "female", 'email': "haruhi@sos-brigade.org", } ok (actual) == expected # if __name__ == "__main__": oktest.main() Above test script reports such as:: AssertionError: --- expected +++ actual @@ -1,3 +1,3 @@ {'email': 'haruhi@sos-brigade.org', - 'gender': 'Female', + 'gender': 'female', 'username': 'Haruhi'} * [enhance] @at_end decorator registers callback which is invoked after test. This is similar to tearDown() method, but more flexible than it. ex:: from oktest import at_end class HomhomTest(unittest.TestCase): def test1(self): ## create dummy file tmpfile = 'dummy.txt' with open(tmpfile, 'w') as f: f.write("blablabla") ## register callback to delete dummy file at end of test case @at_end def _(): os.unlink(tmpfile) ## do test with open(tmpfile) as f: ok (f.read()) == "blablabla" You can use @at_end instead of fixture release functions. ex:: class HomhomTest(unittest.TestCase): ## ## fixture provider -- use @at_end instead of releaser ## def provide_tmpfile(self): ## create dummy file tmpfile = 'dummy.txt' with open(tmpfile, 'w') as f: f.write('blablabla') ## register callback to delete dummy file at end of test case @at_end def _(): os.unlink(tmpfile) ## return fixture data return tmpfile ## ## fixture releaser -- no need to define because we have @at_end! ## #def release_tmpfile(self, tmpfile): # os.unlink(tmpfile) @at_end decorator is similar to unittest.TestCase#atCleanup(), but the former is called *before* tearDown() and the latter is called *after* tearDown(). * [enhance] (experimental) New assertion ``ok(response).is_response()`` to assert WebOb or Werkzeug resonse object. ex:: ok (response).is_response(200) # status code ok (response).is_response((302, 303)) # status code ok (response).is_response('200 OK') # status line ok (response).is_response(200, 'image/jpeg') # content-type ok (response).is_response(200, re.compile(r'^image/(jpeg|png|gif)$')) ok (response).is_response(302).header("Location", "/") # header ok (response).is_response(200).json({"status": "OK"}) # json data ok (response).is_response(200).body("

Hello

") # response body ok (response).is_response(200).body(re.compile("

.*?

")) * [enhance] ``ok(x).is_truthy()`` tests whether ``bool(x) == True``. This is similar to ``ok(bool(x)) == True`` but reports ``x`` value instead of True/False when assertion failed. * [enhance] ``ok(x).is_falsy()`` tests whether ``bool(x) == False``. This is similar to ``ok(bool(x)) == False`` but reports ``x`` value instead of True/False when assertion failed. * [enhance] 'oktest.util.from_here()' is defined which adds current path into sys.path temporarily. This is useful very much when you want to import a certain module from current directory or a specific directory. ex:: from oktest.util import from_here with from_here(): import mymodule1 # import from directory path of this file with from_here('../lib'): import mymodule2 # import from ../lib * [enhance] 'oktest.util.randstr(n)' is defined which returns random number string. Width of returned string is n (default 8). This is very useful when creating fixture data. ex:: >>> from oktest.util import randstr >>> randstr(4) '7327' >>> randstr(4) '1598' >>> randstr(4) '0362' >>> randstr() '38127841' * [enhance] subject() and situation() now accept user-defined tags. ex:: class FooTest(unitest.TestCase): with subject('#method1()', category='basic'): with situation('when argument is None', category='advanced'): @test('returns True') def _(self): ok (Foo().method1('')) == True * [enhance] @test decorator regards '[!xxx]' in description as spec id. ex:: ## python class HomhomTest(unittest.TestCase): @test('[!bk201] 1+1 should be 2') # spec id is 'bk201' def _(self): ok (1+1) == 2 @test('[!nov11] 1-1 should be 0') # spec id is 'nov11' def _(self): ok (1-1) == 0 You can filter tests by spec id (sid). ## command-line bash$ python -m oktest test/example_test.py -f sid=bk201 * [enhance] multiple errors are reported. For example, if you got error on test method and tearDown(), both errors are reported. * [change] '[passed]', '[Failed]', and '[skipped]' are renamed to '[pass]', '[Fail]', '[skip]' respectively. Previous output:: - [passed] 1+1 should be 2. - [Failed] 1-1 should be 0. ## total:1, passed:0, failed:0, error:0, skipped:0, todo:1 (0.000 sec) New version output:: - [pass] 1+1 should be 2. - [Fail] 1-1 should be 0. ## total:1, pass:0, fail:0, error:0, skip:0, todo:1 (0.000 sec) * [bugfix] @test decorator now supports unicode description. ex:: @test(u"日本語") # error on previous version but not in this release def _(self): ok (1+1) == 2 * [bugfix] @todo decorator now supports fixture arguments. ex:: class FooTest(unittest.TestCase): # def provide_x(self): return 123 # @test('example') @todo # error on previous version but not on this release def _(self, x): assert False Release 0.11.1 -------------- * [bugfix] fix '-s verbose' (verbose mode) option to clear long test description. * [bugfix] fix test reporter not to raise UnicodeEncodeError/UnicodeDecodeError. Release 0.11.0 -------------- * [change] 'spec()' is now NOT obsoleted. * [change] 'spec()' is now available as function decorator. ex:: class FooTest(unittest.TestCase): def test_method1(self) @spec("1+1 should be 2") def _(): ok (1+1) == 2 @spec("1-1 should be 0") def _(): ok (1-1) == 0 * [enhance] New assertions: not_file(), not_dir() and not_exist(). ex:: ok (".").not_file() # same as NG (".").is_file() ok (__file__).not_dir() # same as NG (__file__).is_dir() ok ("foobar").not_exist() # same as NG ("foobar").exists() * [enhance] New assertion: not_match(). ex:: ok ("SOS").not_match(r"\d+") # same as NG ("SOS").matches(r"\d+") * [enhance] Global provider/releaser functions can take 'self' argument. ex:: def provide_logname(self): self._LOGNAME = os.getenv('LOGNAME') os.environ['LOGNAME'] = "Haruhi" return os.environ['LOGNAME'] def release_logname(self, value): os.environ['LOGNAME'] = self._LOGNAME * [change] Change not to ignore test classes which name starts with '_'. * [change] (internal) Move some utility functions to 'util' module. * [change] (internal) Move '_Context' and '_RunnableContext' classes into 'util' module. * [change] (internal) Move 'Color' class into 'util' module * [change] (internal) Remove 'OUT' variable in 'Reporter' class * [change] (internal) Move 'TARGET_PATTERN' variable to 'config' * [bugfix] Fix to clear ImportError after trying to import unittest2 Release 0.10.0 -------------- * [change] 'oktest.spec()' is obsoleted completely. It will print warning message if you use it. * [change] 'oktest.helper' module is renamed to 'oktest.util'. ('oktest.helper' is still available for backward compabibility.) * [enhance] Add 'oktest.main()' which is a replacement of 'oktest.run()'. Using 'oktest.main()' instead of 'oktest.run()', command options are available. ex:: ## for example: $ python test/foobar_test.py -sp -f test='*keyword*' ## is almost same as: $ python -m oktest test/foobar_test.py -sp -f test='*keyword*' * [enhance] Add 'oktest.fail(message)' which is same as 'unittest.fail(message)'. ex:: from oktest import fail fail("not impelmented yet") # will raise AssertionError * [enhance] (Experimental) Add '@todo' decorator which is equivarent to '@unittest.expectedFailure'. ex:: from oktest import ok, test, todo def add(x, y): return 0 # not implemented yet! class AddTest(unittest.TestCase): @test("returns sum of arguments.") @todo # equivarent to @unittest.expectedFailure def _(self): ok (10, 20) == 30 ## will be failed expectedly ## (because not implemented yet) Expected failure of assertion is reported as '[TODO]', not '[Failed]'. * [enhance] (Experimental) Test context supported. It helps you to describe specification in structured style. ex:: from oktest import ok, test from oktest import subject, situation class SampleTestCase(unittest.TestCase): SUBJECT = "class 'Sample'" with subject("method1()"): with situation("when condition:"): @test("spec1") def _(self): ... @test("spec2") def _(self): ... with situation("else:"): @test("spec3") def _(self): ... if __name__ == '__main__': import oktest oktest.main() Output exmple:: $ python test/example_test.py * class 'Sample' + method1() + when condition: - [passed] spec1 - [passed] spec2 + else: - [passed] spec3 ## total:3, passed:3, failed:0, error:0, skipped:0, todo:0 (0.000 sec) * [change] Output is changed. ### ### previous version ### $ python test/foo_test.py * FooTest - [ok] test1 - [ok] test2 - [skipped] test3 ## total:3, passed:2, failed:0, error:0, skipped:1 (elapsed 0.000) ### ### in this release ### $ python test/foo_test.py * FooTest - [passed] test1 - [passed] test2 - [skipped] test3 (reason: REASON) ## total:3, passed:2, failed:0, error:0, skipped:1, todo:0 (0.000 sec) Release 0.9.0 ------------- * New '@test' decorator provided. It is simple but very powerful. Using @test decorator, you can write test description in free text instead of test method. ex:: class FooTest(unittest.TestCase): def test_1_plus_1_should_be_2(self): # not cool... self.assertEqual(2, 1+1) @test("1 + 1 should be 2") # cool! easy to read & write! def _(self): self.assertEqual(2, 1+1) * Fixture injection support by '@test' decorator. Arguments of test method are regarded as fixture names and they are injected by @test decorator automatically. Instance methods or global functions which name is 'provide_xxxx' are regarded as fixture provider (or builder) for fixture 'xxxx'. ex:: class SosTest(unittest.TestCase): ## ## fixture providers. ## def provide_member1(self): return {"name": "Haruhi"} def provide_member2(self): return {"name": "Kyon"} ## ## fixture releaser (optional) ## def release_member1(self, value): assert value == {"name": "Haruhi"} ## ## testcase which requires 'member1' and 'member2' fixtures. ## @test("validate member's names") def _(self, member1, member2): assert member1["name"] == "Haruhi" assert member2["name"] == "Kyon" Dependencies between fixtures are resolved automatically. ex:: class BarTest(unittest.TestCase): ## ## for example: ## - Fixture 'a' depends on 'b' and 'c'. ## - Fixture 'c' depends on 'd'. ## def provide_a(b, c): return b + c + ["A"] def provide_b(): return ["B"] def provide_c(d): return d + ["C"] def provide_d(): reutrn ["D"] ## ## Dependencies between fixtures are solved automatically. ## @test("dependency test") def _(self, a): assert a == ["B", "D", "C", "A"] If loop exists in dependency then @test reports error. If you want to integrate with other fixture library, see the following example:: class MyFixtureManager(object): def __init__(self): self.values = { "x": 100, "y": 200 } def provide(self, name): return self.values[name] def release(self, name, value): pass oktest.fixure_manager = MyFixtureResolver() * Supports command-line interface to execute test scripts. ex:: ## run test scripts except foo_*.py in plain style $ python -m oktest -x 'foo_*.py' -sp tests/*_test.py ## run test scripts in 'tests' dir with pattern '*_test.py' $ python -m oktest -p '*_test.py' tests ## filter by class name $ python -m oktest -f class='ClassName*' tests ## filter by test method name $ python -m oktest -f test='*keyword*' tests ## filter by user-defined option added by @test decorator $ python -m oktest -f tag='*value*' tests Try ``python -m oktest -h`` for details about command-line options. * Reporting style is changed. Oktest now provides three reporing styles. - plain (similar to unittest) - simple - verbose (default) All of these styles are colored to emphasize errors. If you want change reporting style, specify ``-r`` option in command-line. * New assertion method ``ok(x).attr(name, value)`` to check attribute. ex:: d = datetime.date(2000, 12, 31) ok (d).attr('year', 2000).attr('month', 12).attr('date', 31) * New assertion method ``ok(x).length(n)``. This is same as ``ok(len(x)) == n``, but it is useful when chaining assertion methods. ex:: ok (func()).is_a(tuple).length(2) * New feature``ok().should`` helps you to check boolean method. ex:: ## same as ok ("Haruhi".startswith("Haru")) == True ok ("Haruhi").should.startswith("Haru") ## same as ok ("Haruhi".isupper()) == False ok ("Haruhi").should_not.isupper() * 'ok(str1) == str2' displays diff if text1 != text2, even when using with unittest module. In previous version, text diff is displayed only when using oktest.run(). If you are unittest user and using Python < 2.7, use 'ok(str1) == str2' instead of 'self.assertEqual(str2, str1)' to display text diff. * Assertion ``raises()`` supports regular expression to check error message. def fn(): raise ValueError("ERROR-123") ok (fn).raises(ValueError, re.compile(r'^[A-Z]+-\d+$')) * Helper functions in oktest.dummy module are now available as decorator. This is useful when you must use Python 2.4:: from oktest.dummy import dummy_io ## for Python 2.4 @dummy_io("SOS") def d_io(): assert sys.stdin.read() == "SOS" print("Haruhi") sout, serr = d_io assert sout == "Haruhi\n" ## for Python 2.5 or later with dummy_io("SOS") as d_io: assert sys.stdin.read() == "SOS" print("Haruhi") sout, serr = d_io assert sout == "Haruhi\n" * 'AssertionObject.expected' is renamed to 'AssertionObject.boolean'. You should update your custom assertion definition. ex:: import oktest @oktest.assertion def startswith(self, arg): boolean = self.target.startswith(arg) #if boolean == self.expected: # obsolete if boolean == self.boolean: return True self.failed("%r.startswith(%r) : failed." % (self.target, arg)) * ``oktest.run()`` is changed to return number of failures and errors of tests.:: sys.exit(oktest.run()) # status code == number of failures and errors * ``before_each()`` and ``after_each()`` are now non-supported. Use ``before()`` and ``after()`` intead. * (Experimental) New function ``NOT()`` provided which is same as ``NG()``. * (Experimental) ``skip()`` and ``@skip.when()`` are provided to skip tests:: from oktest import skip class FooTest(unittest.TestCase): def test_1(self): if sys.version.startswith('2.'): reason = "not available on Python 2.x" skip(reason) # raises SkipTest ... @skip.when(sys.version.startswith('2.'), "not available on Python 2.x") def test_2(self): ... If you want to use @skip.when with @test decorator, see the folloing:: ## OK @test("description") @skip.when(condition, "reason") def _(self): ... ## NG @skip.when(condition, "reason") @test("description") def _(self): ... Release 0.8.0 ------------- * add ``NG()`` which is same as not_ok(). * enhanced to proive egg files for Python 3. * enhanced to support assertion method chaining. :: ok ("sos".upper()).is_a(str).matches(r'^[A-Z]+$') == "SOS" * ``ok().matches()`` can take flag parameter which is passed to re.compile(). ok ("\nSOS\n").matches(r'^[A-Z]+$', re.M) ## same as: #ok("\nSOS\n").matches(r.compile(r'^[A-Z]$', re.M)) * enhance helper methods to be available without with-statement. (this is necessary for Python 2.4 which is default version on CentOS.) from oktest.helper import chdir def fn(): ok (os.getcwd()) == "/tmp" chdir("/tmp").run(fn) ## this is same as: #with chdir("/tmp"): # ok (os.getcwd()) == "/tmp" from oktest.dummy import dummy_file def fn(): ok ("A.txt").is_file() ok (open("A.txt").read()) == "SOS" dummy_file("A.txt", "SOS").run(fun) ## this is same as: #with dummy_file("A.txt", "SOS"): # ok (open("A.txt").read()) == "SOS" * ``spec()`` now checks environment variable $SPEC. This is useful to filter test cases. ## test script from oktest import oktest, run class StrTest(object): def test_upper(self): if spec("returns upper case string"): ok ("sos".upper()) == "SOS" if spec("doesn't change non-alphabetics"): ok ("sos123<>".upper()) == "SOS123<>" if __name__ == "__main__": run() ## terminal $ SPEC="returns upper case string" python test1.py * fix ``oktest.run()`` to print correct traceback if ok() is called from nested function. * fix content of README.txt. Release 0.7.0 ------------- * enhanced to allow users to define custom assertion functions. :: import oktest from oktest import ok # @oktest.assertion def startswith(self, arg): boolean = self.target.startswith(arg) if boolean == self.expected: return True self.failed("%r.startswith(%r) : failed." % (self.target, arg)) # ok ("Sasaki").startswith("Sas") * rename 'ok().hasattr()' to 'ok().has_attr()'. (but old name is also available for backward compatibility.) * change 'chdir()' to take a function as 2nd argument. :: def f(): ... do_something ... chdir('build', f) # The above is same as: with chdir('build'): ... do_something ... * add document of 'oktest.helper.dummy_io()'. :: with dummy_io("SOS") as io: assert sys.stdin.read() == "SOS" print("Haruhi") assert io.stdout == "Haruhi\n" assert io.stderr == "" * fix 'oktest.tracer.Call#__repr__()' to change output according to whether '==' is called or not. This is aimed to make output of 'ok(tr[0]) == [...]' to be more readable. * change 'Runner#run()' to skip AssertionError if it is raised by 'assert ....' and not 'ok() == ...'. Release 0.6.0 ------------- * enhanced to add 'oktest.tracer.Tracer' class. see README for details. * change 'run()' to sort classes order by lineno in source file. * change default argument of 'run()' from '.*Test(Case)$' to '.*(Test|TestCase|_TC)$'. Release 0.5.0 ------------- * change default argument of 'run()' to '.*Test(Case)$'. * enhanced to report untested AssertionObject. * new helper function 'spec()' which describes test specification. * new helper function 'dummy_values()' which changes dictionary temporarily. * new helper function 'dummy_attrs()' which changes object's attributes temporarily. * 'TestCaseRunner' class is renamed to 'TestRunner'. * (undocumented) new helper function 'dummy_environ_vars()'. * (undocumented) new helper function 'using()'. * (uncodumented) add rm_rf() Release 0.4.0 ------------- * enhanced to support 'ok (x).in_delta(y, d)' which raises assertion exception unless y-d < x < y+d. * change test script to support Python 2.7 and 3.x. * fixed several bugs. Release 0.3.0 ------------- * enhanced 'ok (s1) == s2' to display unified diff (diff -u) * changed to call 'before()/after()' instead of 'before_each()/after_each()' (currently 'before_each()/after_each()' is also enabled but they will be disabled in the future) * improved compatibility with unittest module * (internal) 'ValueObject' class is renamed to 'AssertionObject' * (internal) 'Reporter#before_each()' and '#after_each()' are renamed into '#before()' and '#after()' Release 0.2.2 ------------- * enhanced to set 'f.exception' after 'ok (f).raises(Exception)' to get raised exception object * changed to flush output after '.'/'f'/'E' printed * change to get exception message by 'str(ex)' instead of 'ex.message' Release 0.2.1 ------------- * fix 'REAMDE.txt' * fix 'setup.py' Release 0.2.0 ------------- * public release