├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── docs ├── index.html └── ldoc.css ├── example.lua ├── lester.lua ├── rockspecs ├── lester-0.1.5-1.rockspec └── lester-dev-1.rockspec └── tests.lua /.gitignore: -------------------------------------------------------------------------------- 1 | *.out 2 | *.src.rock -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021-2023 Eduardo Bart (https://github.com/edubart) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | ROCKSPEC=rockspecs/lester-0.*.rockspec 2 | LUA=lua 3 | LUACOV=luacov 4 | LUALCOV=$(LUA) -lluacov 5 | 6 | test: 7 | LESTER_TEST_SKIP_FAIL=true $(LUA) tests.lua 8 | 9 | coverage: 10 | rm -f luacov.stats.out 11 | $(LUA) -lluacov tests.lua 12 | LESTER_QUIET=true $(LUALCOV) tests.lua 13 | LESTER_COLOR=false $(LUALCOV) tests.lua 14 | LESTER_SHOW_TRACEBACK=false $(LUALCOV) tests.lua 15 | LESTER_SHOW_ERROR=false $(LUALCOV) tests.lua 16 | LESTER_STOP_ON_FAIL=true $(LUALCOV) tests.lua || true 17 | LESTER_QUIET=true LESTER_STOP_ON_FAIL=true $(LUALCOV) tests.lua || true 18 | LESTER_FILTER="nested" LESTER_TEST_SKIP_FAIL=true $(LUALCOV) tests.lua 19 | LESTER_TEST_SKIP_FAIL=true $(LUALCOV) tests.lua 20 | LESTER_TEST_SKIP_FAIL=true LESTER_QUIET=true $(LUALCOV) tests.lua 21 | 22 | $(LUALCOV) tests.lua --quiet 23 | $(LUALCOV) tests.lua --no-color 24 | $(LUALCOV) tests.lua --no-show-traceback 25 | $(LUALCOV) tests.lua --no-show-error 26 | $(LUALCOV) tests.lua --stop-on-fail|| true 27 | $(LUALCOV) tests.lua --quiet --stop-on-fail || true 28 | $(LUALCOV) tests.lua --filter="nested" --test-skip-fail 29 | LESTER_TEST_SKIP_FAIL=true $(LUALCOV) tests.lua 30 | LESTER_TEST_SKIP_FAIL=true $(LUALCOV) tests.lua --quiet 31 | 32 | $(LUACOV) 33 | tail -n 6 luacov.report.out 34 | 35 | docs: 36 | ldoc -d docs -f markdown -t "Lester Reference" lester.lua 37 | 38 | install: 39 | luarocks make --local 40 | 41 | upload: 42 | luarocks upload --api-key=$(LUAROCKS_APIKEY) $(ROCKSPEC) 43 | 44 | clean: 45 | rm -f *.out 46 | 47 | .PHONY: docs 48 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Lester 2 | 3 | Minimal Lua test framework. 4 | 5 | Lester is a minimal unit testing framework for Lua with a focus on being simple to use. 6 | 7 | It is highly inspired by 8 | [Busted](http://olivinelabs.com/busted/) and [Lust](https://github.com/bjornbytes/lust). 9 | It was mainly created to replace Busted without dependencies in the 10 | [Nelua](https://github.com/edubart/nelua-lang) compiler. 11 | 12 | [![asciicast](https://asciinema.org/a/GihfI07vCt9Q7cvL6xCtnoNl1.svg)](https://asciinema.org/a/GihfI07vCt9Q7cvL6xCtnoNl1) 13 | 14 | ## Features 15 | 16 | * Minimal, just one file. 17 | * Self contained, no external dependencies. 18 | * Simple and hackable when needed. 19 | * Use `describe` and `it` blocks to describe tests. 20 | * Supports `before` and `after` handlers. 21 | * Supports marking tests as disabled to be skipped. 22 | * Colored output. 23 | * Configurable via the script or with environment variables. 24 | * Quiet mode, to use in live development. 25 | * Optionally filter tests by name. 26 | * Show traceback on errors. 27 | * Show time to complete tests. 28 | * Works with Lua 5.1+. 29 | * Efficient. 30 | 31 | ## Usage 32 | 33 | Copy `lester.lua` file to a project and require it, 34 | which returns a table that includes all of the functionality: 35 | 36 | ```lua 37 | local lester = require 'lester' 38 | local describe, it, expect = lester.describe, lester.it, lester.expect 39 | 40 | -- Customize lester configuration. 41 | lester.show_traceback = false 42 | 43 | describe('my project', function() 44 | lester.before(function() 45 | -- This function is run before every test. 46 | end) 47 | 48 | describe('module1', function() -- Describe blocks can be nested. 49 | it('feature1', function() 50 | expect.equal('something', 'something') -- Pass. 51 | end) 52 | 53 | it('feature2', function() 54 | expect.truthy(false) -- Fail. 55 | end) 56 | 57 | local feature3_test_enabled = false 58 | it('feature3', function() -- This test will be skipped. 59 | expect.truthy(false) -- Fail. 60 | end, feature3_test_enabled) 61 | end) 62 | end) 63 | 64 | lester.report() -- Print overall statistic of the tests run. 65 | lester.exit() -- Exit with success if all tests passed. 66 | ``` 67 | 68 | ## Customizing output with environment variables 69 | 70 | To customize the output of lester externally, 71 | you can set the following environment variables before running a test suite: 72 | 73 | * `LESTER_QUIET="true"`, omit print of passed tests. 74 | * `LESTER_COLORED="false"`, disable colored output. 75 | * `LESTER_SHOW_TRACEBACK="false"`, disable traceback on test failures. 76 | * `LESTER_SHOW_ERROR="false"`, omit print of error description of failed tests. 77 | * `LESTER_STOP_ON_FAIL="true"`, stop on first test failure. 78 | * `LESTER_UTF8TERM="false"`, disable printing of UTF-8 characters. 79 | * `LESTER_FILTER="some text"`, filter the tests that should be run. 80 | 81 | Note that these configurations can be changed via script too, check the documentation. 82 | 83 | ## Customizing output with command line arguments 84 | 85 | You can also customize output using command line arguments 86 | if `lester.parse_args()` is called at startup. 87 | 88 | The following command line arguments are available: 89 | 90 | * `--quiet`, omit print of passed tests. 91 | * `--no-quiet`, show print of passed tests. 92 | * `--no-color`, disable colored output. 93 | * `--no-show-traceback`, disable traceback on test failures. 94 | * `--no-show-error`, omit print of error description of failed tests. 95 | * `--stop-on-fail`, stop on first test failure. 96 | * `--no-utf8term`, disable printing of UTF-8 characters. 97 | * `--filter="some text"`, filter the tests that should be run. 98 | 99 | ## Documentation 100 | 101 | The full API reference and documentation can be viewed in the 102 | [documentation website](https://edubart.github.io/lester/). 103 | 104 | ## Install 105 | 106 | You can use luarocks to install quickly: 107 | 108 | ```bash 109 | luarocks install lester 110 | ``` 111 | 112 | Or just copy the `lester.lua` file, the library is self contained in this single file with no dependencies. 113 | 114 | ## Tests 115 | 116 | To check if everything is working as expected under your machine run `lua tests.lua` or `make test`. 117 | 118 | ## License 119 | 120 | MIT, see LICENSE for details. 121 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | Lester Reference 7 | 8 | 9 | 10 | 11 |
12 | 13 |
14 | 15 |
16 |
17 |
18 | 19 | 20 |
21 | 22 | 23 | 24 | 25 | 44 | 45 |
46 | 47 |

Module lester

48 |

Lester is a minimal unit testing framework for Lua with a focus on being simple to use.

49 |

50 | 51 | 52 |

Features

53 | 54 |
    55 |
  • Minimal, just one file.
  • 56 |
  • Self contained, no external dependencies.
  • 57 |
  • Simple and hackable when needed.
  • 58 |
  • Use describe and it blocks to describe tests.
  • 59 |
  • Supports before and after handlers.
  • 60 |
  • Colored output.
  • 61 |
  • Configurable via the script or with environment variables.
  • 62 |
  • Quiet mode, to use in live development.
  • 63 |
  • Optionally filter tests by name.
  • 64 |
  • Show traceback on errors.
  • 65 |
  • Show time to complete tests.
  • 66 |
  • Works with Lua 5.1+.
  • 67 |
  • Efficient.
  • 68 |
69 | 70 |

Usage

71 | 72 |

Copy lester.lua file to a project and require it, 73 | which returns a table that includes all of the functionality:

74 | 75 | 76 |
 77 | local lester = require 'lester'
 78 | local describe, it, expect = lester.describe, lester.it, lester.expect
 79 | 
 80 | -- Customize lester configuration.
 81 | lester.show_traceback = false
 82 | 
 83 | -- Parse arguments from command line.
 84 | lester.parse_args()
 85 | 
 86 | describe('my project', function()
 87 |   lester.before(function()
 88 |     -- This function is run before every test.
 89 |   end)
 90 | 
 91 |   describe('module1', function() -- Describe blocks can be nested.
 92 |     it('feature1', function()
 93 |       expect.equal('something', 'something') -- Pass.
 94 |     end)
 95 | 
 96 |     it('feature2', function()
 97 |       expect.truthy(false) -- Fail.
 98 |     end)
 99 | 
100 |     local feature3_test_enabled = false
101 |     it('feature3', function() -- This test will be skipped.
102 |       expect.truthy(false) -- Fail.
103 |     end, feature3_test_enabled)
104 |   end)
105 | end)
106 | 
107 | lester.report() -- Print overall statistic of the tests run.
108 | lester.exit() -- Exit with success if all tests passed.
109 | 
110 | 111 | 112 |

Customizing output with environment variables

113 | 114 |

To customize the output of lester externally, 115 | you can set the following environment variables before running a test suite:

116 | 117 |
    118 |
  • LESTER_QUIET="true", omit print of passed tests.
  • 119 |
  • LESTER_COLOR="false", disable colored output.
  • 120 |
  • LESTER_SHOW_TRACEBACK="false", disable traceback on test failures.
  • 121 |
  • LESTER_SHOW_ERROR="false", omit print of error description of failed tests.
  • 122 |
  • LESTER_STOP_ON_FAIL="true", stop on first test failure.
  • 123 |
  • LESTER_UTF8TERM="false", disable printing of UTF-8 characters.
  • 124 |
  • LESTER_FILTER="some text", filter the tests that should be run.
  • 125 |
126 | 127 |

Note that these configurations can be changed via script too, check the documentation.

128 | 129 |

Customizing output with command line arguments

130 | 131 |

You can also customize output using command line arguments 132 | if lester.parse_args() is called at startup.

133 | 134 |

The following command line arguments are available:

135 | 136 |
    137 |
  • --quiet, omit print of passed tests.
  • 138 |
  • --no-quiet, show print of passed tests.
  • 139 |
  • --no-color, disable colored output.
  • 140 |
  • --no-show-traceback, disable traceback on test failures.
  • 141 |
  • --no-show-error, omit print of error description of failed tests.
  • 142 |
  • --stop-on-fail, stop on first test failure.
  • 143 |
  • --no-utf8term, disable printing of UTF-8 characters.
  • 144 |
  • --filter="some text", filter the tests that should be run.
  • 145 |
146 |

147 | 148 | 149 |

Functions

150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 |
describe (name, func)Describe a block of tests, which consists in a set of tests.
it (name, func, enabled)Declare a test, which consists of a set of assertions.
before (func)Set a function that is called before every test inside a describe block.
after (func)Set a function that is called after every test inside a describe block.
report ()Pretty print statistics of all test runs.
exit ()Exit the application with success code if all tests passed, or failure code otherwise.
expect.tohumanstring (v)Converts a value to a human-readable string.
expect.fail (func, expected)Check if a function fails with an error.
expect.not_fail (func)Check if a function does not fail with a error.
expect.exist (v)Check if a value is not nil.
expect.not_exist (v)Check if a value is nil.
expect.truthy (v)Check if an expression is evaluates to true.
expect.falsy (v)Check if an expression is evaluates to false.
expect.strict_eq (t1, t2, name)Compare if two values are equal, considering nested tables.
expect.equal (v1, v2)Check if two values are equal.
expect.not_equal (v1, v2)Check if two values are not equal.
216 |

Fields

217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 |
quietWhether lines of passed tests should not be printed.
colorWhether the output should be colorized.
show_tracebackWhether a traceback must be shown on test failures.
show_errorWhether the error description of a test failure should be shown.
stop_on_failWhether test suite should exit on first test failure.
utf8termWhether we can print UTF-8 characters to the terminal.
filterA string with a lua pattern to filter tests.
secondsFunction to retrieve time in seconds with milliseconds precision, os.clock by default.
colorsTable of terminal colors codes, can be customized.
expectExpect module, containing utility function for doing assertions inside a test.
259 | 260 |
261 |
262 | 263 | 264 |

Functions

265 | 266 |
267 |
268 | 269 | describe (name, func) 270 |
271 |
272 | Describe a block of tests, which consists in a set of tests. 273 | Describes can be nested. 274 | 275 | 276 |

Parameters:

277 |
    278 |
  • name 279 | A string used to describe the block. 280 |
  • 281 |
  • func 282 | A function containing all the tests or other describes. 283 |
  • 284 |
285 | 286 | 287 | 288 | 289 | 290 |
291 |
292 | 293 | it (name, func, enabled) 294 |
295 |
296 | Declare a test, which consists of a set of assertions. 297 | 298 | 299 |

Parameters:

300 |
    301 |
  • name 302 | A name for the test. 303 |
  • 304 |
  • func 305 | The function containing all assertions. 306 |
  • 307 |
  • enabled 308 | If not nil and equals to false, the test will be skipped and this will be reported. 309 |
  • 310 |
311 | 312 | 313 | 314 | 315 | 316 |
317 |
318 | 319 | before (func) 320 |
321 |
322 | Set a function that is called before every test inside a describe block. 323 | A single string containing the name of the test about to be run will be passed to func. 324 | 325 | 326 |

Parameters:

327 |
    328 |
  • func 329 | 330 | 331 | 332 |
  • 333 |
334 | 335 | 336 | 337 | 338 | 339 |
340 |
341 | 342 | after (func) 343 |
344 |
345 | Set a function that is called after every test inside a describe block. 346 | A single string containing the name of the test that was finished will be passed to func. 347 | The function is executed independently if the test passed or failed. 348 | 349 | 350 |

Parameters:

351 |
    352 |
  • func 353 | 354 | 355 | 356 |
  • 357 |
358 | 359 | 360 | 361 | 362 | 363 |
364 |
365 | 366 | report () 367 |
368 |
369 | Pretty print statistics of all test runs. 370 | With total success, total failures and run time in seconds. 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 |
379 |
380 | 381 | exit () 382 |
383 |
384 | Exit the application with success code if all tests passed, or failure code otherwise. 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 |
393 |
394 | 395 | expect.tohumanstring (v) 396 |
397 |
398 | Converts a value to a human-readable string. 399 | If the final string not contains only ASCII characters, 400 | then it is converted to a Lua hexdecimal string. 401 | 402 | 403 |

Parameters:

404 |
    405 |
  • v 406 | 407 | 408 | 409 |
  • 410 |
411 | 412 | 413 | 414 | 415 | 416 |
417 |
418 | 419 | expect.fail (func, expected) 420 |
421 |
422 | Check if a function fails with an error. 423 | If expected is nil then any error is accepted. 424 | If expected is a string then we check if the error contains that string. 425 | If expected is anything else then we check if both are equal. 426 | 427 | 428 |

Parameters:

429 |
    430 |
  • func 431 | 432 | 433 | 434 |
  • 435 |
  • expected 436 | 437 | 438 | 439 |
  • 440 |
441 | 442 | 443 | 444 | 445 | 446 |
447 |
448 | 449 | expect.not_fail (func) 450 |
451 |
452 | Check if a function does not fail with a error. 453 | 454 | 455 |

Parameters:

456 |
    457 |
  • func 458 | 459 | 460 | 461 |
  • 462 |
463 | 464 | 465 | 466 | 467 | 468 |
469 |
470 | 471 | expect.exist (v) 472 |
473 |
474 | Check if a value is not nil. 475 | 476 | 477 |

Parameters:

478 |
    479 |
  • v 480 | 481 | 482 | 483 |
  • 484 |
485 | 486 | 487 | 488 | 489 | 490 |
491 |
492 | 493 | expect.not_exist (v) 494 |
495 |
496 | Check if a value is nil. 497 | 498 | 499 |

Parameters:

500 |
    501 |
  • v 502 | 503 | 504 | 505 |
  • 506 |
507 | 508 | 509 | 510 | 511 | 512 |
513 |
514 | 515 | expect.truthy (v) 516 |
517 |
518 | Check if an expression is evaluates to true. 519 | 520 | 521 |

Parameters:

522 |
    523 |
  • v 524 | 525 | 526 | 527 |
  • 528 |
529 | 530 | 531 | 532 | 533 | 534 |
535 |
536 | 537 | expect.falsy (v) 538 |
539 |
540 | Check if an expression is evaluates to false. 541 | 542 | 543 |

Parameters:

544 |
    545 |
  • v 546 | 547 | 548 | 549 |
  • 550 |
551 | 552 | 553 | 554 | 555 | 556 |
557 |
558 | 559 | expect.strict_eq (t1, t2, name) 560 |
561 |
562 | Compare if two values are equal, considering nested tables. 563 | 564 | 565 |

Parameters:

566 |
    567 |
  • t1 568 | 569 | 570 | 571 |
  • 572 |
  • t2 573 | 574 | 575 | 576 |
  • 577 |
  • name 578 | 579 | 580 | 581 |
  • 582 |
583 | 584 | 585 | 586 | 587 | 588 |
589 |
590 | 591 | expect.equal (v1, v2) 592 |
593 |
594 | Check if two values are equal. 595 | 596 | 597 |

Parameters:

598 |
    599 |
  • v1 600 | 601 | 602 | 603 |
  • 604 |
  • v2 605 | 606 | 607 | 608 |
  • 609 |
610 | 611 | 612 | 613 | 614 | 615 |
616 |
617 | 618 | expect.not_equal (v1, v2) 619 |
620 |
621 | Check if two values are not equal. 622 | 623 | 624 |

Parameters:

625 |
    626 |
  • v1 627 | 628 | 629 | 630 |
  • 631 |
  • v2 632 | 633 | 634 | 635 |
  • 636 |
637 | 638 | 639 | 640 | 641 | 642 |
643 |
644 |

Fields

645 | 646 |
647 |
648 | 649 | quiet 650 |
651 |
652 | Whether lines of passed tests should not be printed. False by default. 653 | 654 | 655 | 656 | 657 | 658 | 659 | 660 |
661 |
662 | 663 | color 664 |
665 |
666 | Whether the output should be colorized. True by default. 667 | 668 | 669 | 670 | 671 | 672 | 673 | 674 |
675 |
676 | 677 | show_traceback 678 |
679 |
680 | Whether a traceback must be shown on test failures. True by default. 681 | 682 | 683 | 684 | 685 | 686 | 687 | 688 |
689 |
690 | 691 | show_error 692 |
693 |
694 | Whether the error description of a test failure should be shown. True by default. 695 | 696 | 697 | 698 | 699 | 700 | 701 | 702 |
703 |
704 | 705 | stop_on_fail 706 |
707 |
708 | Whether test suite should exit on first test failure. False by default. 709 | 710 | 711 | 712 | 713 | 714 | 715 | 716 |
717 |
718 | 719 | utf8term 720 |
721 |
722 | Whether we can print UTF-8 characters to the terminal. True by default when supported. 723 | 724 | 725 | 726 | 727 | 728 | 729 | 730 |
731 |
732 | 733 | filter 734 |
735 |
736 | A string with a lua pattern to filter tests. Nil by default. 737 | 738 | 739 | 740 | 741 | 742 | 743 | 744 |
745 |
746 | 747 | seconds 748 |
749 |
750 | Function to retrieve time in seconds with milliseconds precision, os.clock by default. 751 | 752 | 753 | 754 | 755 | 756 | 757 | 758 |
759 |
760 | 761 | colors 762 |
763 |
764 | Table of terminal colors codes, can be customized. 765 | 766 | 767 | 768 | 769 | 770 | 771 | 772 |
773 |
774 | 775 | expect 776 |
777 |
778 | Expect module, containing utility function for doing assertions inside a test. 779 | 780 | 781 | 782 | 783 | 784 | 785 | 786 |
787 |
788 | 789 | 790 |
791 |
792 |
793 | generated by LDoc 1.5.0 794 | Last updated 2023-10-18 08:06:51 795 |
796 |
797 | 798 | 799 | -------------------------------------------------------------------------------- /docs/ldoc.css: -------------------------------------------------------------------------------- 1 | /* BEGIN RESET 2 | 3 | Copyright (c) 2010, Yahoo! Inc. All rights reserved. 4 | Code licensed under the BSD License: 5 | http://developer.yahoo.com/yui/license.html 6 | version: 2.8.2r1 7 | */ 8 | html { 9 | color: #000; 10 | background: #FFF; 11 | } 12 | body,div,dl,dt,dd,ul,ol,li,h1,h2,h3,h4,h5,h6,pre,code,form,fieldset,legend,input,button,textarea,p,blockquote,th,td { 13 | margin: 0; 14 | padding: 0; 15 | } 16 | table { 17 | border-collapse: collapse; 18 | border-spacing: 0; 19 | } 20 | fieldset,img { 21 | border: 0; 22 | } 23 | address,caption,cite,code,dfn,em,strong,th,var,optgroup { 24 | font-style: inherit; 25 | font-weight: inherit; 26 | } 27 | del,ins { 28 | text-decoration: none; 29 | } 30 | li { 31 | margin-left: 20px; 32 | } 33 | caption,th { 34 | text-align: left; 35 | } 36 | h1,h2,h3,h4,h5,h6 { 37 | font-size: 100%; 38 | font-weight: bold; 39 | } 40 | q:before,q:after { 41 | content: ''; 42 | } 43 | abbr,acronym { 44 | border: 0; 45 | font-variant: normal; 46 | } 47 | sup { 48 | vertical-align: baseline; 49 | } 50 | sub { 51 | vertical-align: baseline; 52 | } 53 | legend { 54 | color: #000; 55 | } 56 | input,button,textarea,select,optgroup,option { 57 | font-family: inherit; 58 | font-size: inherit; 59 | font-style: inherit; 60 | font-weight: inherit; 61 | } 62 | input,button,textarea,select {*font-size:100%; 63 | } 64 | /* END RESET */ 65 | 66 | body { 67 | margin-left: 1em; 68 | margin-right: 1em; 69 | font-family: arial, helvetica, geneva, sans-serif; 70 | background-color: #ffffff; margin: 0px; 71 | } 72 | 73 | code, tt { font-family: monospace; font-size: 1.1em; } 74 | span.parameter { font-family:monospace; } 75 | span.parameter:after { content:":"; } 76 | span.types:before { content:"("; } 77 | span.types:after { content:")"; } 78 | .type { font-weight: bold; font-style:italic } 79 | 80 | body, p, td, th { font-size: .95em; line-height: 1.2em;} 81 | 82 | p, ul { margin: 10px 0 0 0px;} 83 | 84 | strong { font-weight: bold;} 85 | 86 | em { font-style: italic;} 87 | 88 | h1 { 89 | font-size: 1.5em; 90 | margin: 20px 0 20px 0; 91 | } 92 | h2, h3, h4 { margin: 15px 0 10px 0; } 93 | h2 { font-size: 1.25em; } 94 | h3 { font-size: 1.15em; } 95 | h4 { font-size: 1.06em; } 96 | 97 | a:link { font-weight: bold; color: #004080; text-decoration: none; } 98 | a:visited { font-weight: bold; color: #006699; text-decoration: none; } 99 | a:link:hover { text-decoration: underline; } 100 | 101 | hr { 102 | color:#cccccc; 103 | background: #00007f; 104 | height: 1px; 105 | } 106 | 107 | blockquote { margin-left: 3em; } 108 | 109 | ul { list-style-type: disc; } 110 | 111 | p.name { 112 | font-family: "Andale Mono", monospace; 113 | padding-top: 1em; 114 | } 115 | 116 | pre { 117 | background-color: rgb(245, 245, 245); 118 | border: 1px solid #C0C0C0; /* silver */ 119 | padding: 10px; 120 | margin: 10px 0 10px 0; 121 | overflow: auto; 122 | font-family: "Andale Mono", monospace; 123 | } 124 | 125 | pre.example { 126 | font-size: .85em; 127 | } 128 | 129 | table.index { border: 1px #00007f; } 130 | table.index td { text-align: left; vertical-align: top; } 131 | 132 | #container { 133 | margin-left: 1em; 134 | margin-right: 1em; 135 | background-color: #f0f0f0; 136 | } 137 | 138 | #product { 139 | text-align: center; 140 | border-bottom: 1px solid #cccccc; 141 | background-color: #ffffff; 142 | } 143 | 144 | #product big { 145 | font-size: 2em; 146 | } 147 | 148 | #main { 149 | background-color: #f0f0f0; 150 | border-left: 2px solid #cccccc; 151 | } 152 | 153 | #navigation { 154 | float: left; 155 | width: 14em; 156 | vertical-align: top; 157 | background-color: #f0f0f0; 158 | overflow: visible; 159 | } 160 | 161 | #navigation h2 { 162 | background-color:#e7e7e7; 163 | font-size:1.1em; 164 | color:#000000; 165 | text-align: left; 166 | padding:0.2em; 167 | border-top:1px solid #dddddd; 168 | border-bottom:1px solid #dddddd; 169 | } 170 | 171 | #navigation ul 172 | { 173 | font-size:1em; 174 | list-style-type: none; 175 | margin: 1px 1px 10px 1px; 176 | } 177 | 178 | #navigation li { 179 | text-indent: -1em; 180 | display: block; 181 | margin: 3px 0px 0px 22px; 182 | } 183 | 184 | #navigation li li a { 185 | margin: 0px 3px 0px -1em; 186 | } 187 | 188 | #content { 189 | margin-left: 14em; 190 | padding: 1em; 191 | width: 700px; 192 | border-left: 2px solid #cccccc; 193 | border-right: 2px solid #cccccc; 194 | background-color: #ffffff; 195 | } 196 | 197 | #about { 198 | clear: both; 199 | padding: 5px; 200 | border-top: 2px solid #cccccc; 201 | background-color: #ffffff; 202 | } 203 | 204 | @media print { 205 | body { 206 | font: 12pt "Times New Roman", "TimeNR", Times, serif; 207 | } 208 | a { font-weight: bold; color: #004080; text-decoration: underline; } 209 | 210 | #main { 211 | background-color: #ffffff; 212 | border-left: 0px; 213 | } 214 | 215 | #container { 216 | margin-left: 2%; 217 | margin-right: 2%; 218 | background-color: #ffffff; 219 | } 220 | 221 | #content { 222 | padding: 1em; 223 | background-color: #ffffff; 224 | } 225 | 226 | #navigation { 227 | display: none; 228 | } 229 | pre.example { 230 | font-family: "Andale Mono", monospace; 231 | font-size: 10pt; 232 | page-break-inside: avoid; 233 | } 234 | } 235 | 236 | table.module_list { 237 | border-width: 1px; 238 | border-style: solid; 239 | border-color: #cccccc; 240 | border-collapse: collapse; 241 | } 242 | table.module_list td { 243 | border-width: 1px; 244 | padding: 3px; 245 | border-style: solid; 246 | border-color: #cccccc; 247 | } 248 | table.module_list td.name { background-color: #f0f0f0; min-width: 200px; } 249 | table.module_list td.summary { width: 100%; } 250 | 251 | 252 | table.function_list { 253 | border-width: 1px; 254 | border-style: solid; 255 | border-color: #cccccc; 256 | border-collapse: collapse; 257 | } 258 | table.function_list td { 259 | border-width: 1px; 260 | padding: 3px; 261 | border-style: solid; 262 | border-color: #cccccc; 263 | } 264 | table.function_list td.name { background-color: #f0f0f0; min-width: 200px; } 265 | table.function_list td.summary { width: 100%; } 266 | 267 | ul.nowrap { 268 | overflow:auto; 269 | white-space:nowrap; 270 | } 271 | 272 | dl.table dt, dl.function dt {border-top: 1px solid #ccc; padding-top: 1em;} 273 | dl.table dd, dl.function dd {padding-bottom: 1em; margin: 10px 0 0 20px;} 274 | dl.table h3, dl.function h3 {font-size: .95em;} 275 | 276 | /* stop sublists from having initial vertical space */ 277 | ul ul { margin-top: 0px; } 278 | ol ul { margin-top: 0px; } 279 | ol ol { margin-top: 0px; } 280 | ul ol { margin-top: 0px; } 281 | 282 | /* make the target distinct; helps when we're navigating to a function */ 283 | a:target + * { 284 | background-color: #FF9; 285 | } 286 | 287 | 288 | /* styles for prettification of source */ 289 | pre .comment { color: #558817; } 290 | pre .constant { color: #a8660d; } 291 | pre .escape { color: #844631; } 292 | pre .keyword { color: #aa5050; font-weight: bold; } 293 | pre .library { color: #0e7c6b; } 294 | pre .marker { color: #512b1e; background: #fedc56; font-weight: bold; } 295 | pre .string { color: #8080ff; } 296 | pre .number { color: #f8660d; } 297 | pre .function-name { color: #60447f; } 298 | pre .operator { color: #2239a8; font-weight: bold; } 299 | pre .preprocessor, pre .prepro { color: #a33243; } 300 | pre .global { color: #800080; } 301 | pre .user-keyword { color: #800080; } 302 | pre .prompt { color: #558817; } 303 | pre .url { color: #272fc2; text-decoration: underline; } 304 | 305 | -------------------------------------------------------------------------------- /example.lua: -------------------------------------------------------------------------------- 1 | local lester = require 'lester' 2 | local describe, it, expect = lester.describe, lester.it, lester.expect 3 | 4 | -- Customize lester configuration. 5 | lester.show_traceback = false 6 | 7 | -- Parse arguments from command line. 8 | lester.parse_args() 9 | 10 | describe('my project', function() 11 | lester.before(function() 12 | -- This function is run before every test. 13 | end) 14 | 15 | describe('module1', function() -- Describe blocks can be nested. 16 | it('feature1', function() 17 | expect.equal('something', 'something') -- Pass. 18 | end) 19 | 20 | it('feature2', function() 21 | expect.truthy(false) -- Fail. 22 | end) 23 | 24 | local feature3_test_enabled = false 25 | it('feature3', function() -- This test will be skipped. 26 | expect.truthy(false) -- Fail. 27 | end, feature3_test_enabled) 28 | end) 29 | end) 30 | 31 | lester.report() -- Print overall statistic of the tests run. 32 | lester.exit() -- Exit with success if all tests passed. 33 | -------------------------------------------------------------------------------- /lester.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Minimal test framework for Lua. 3 | lester - v0.1.5 - 18/Oct/2023 4 | Eduardo Bart - edub4rt@gmail.com 5 | https://github.com/edubart/lester 6 | Minimal Lua test framework. 7 | See end of file for LICENSE. 8 | ]] 9 | 10 | --[[-- 11 | Lester is a minimal unit testing framework for Lua with a focus on being simple to use. 12 | 13 | ## Features 14 | 15 | * Minimal, just one file. 16 | * Self contained, no external dependencies. 17 | * Simple and hackable when needed. 18 | * Use `describe` and `it` blocks to describe tests. 19 | * Supports `before` and `after` handlers. 20 | * Colored output. 21 | * Configurable via the script or with environment variables. 22 | * Quiet mode, to use in live development. 23 | * Optionally filter tests by name. 24 | * Show traceback on errors. 25 | * Show time to complete tests. 26 | * Works with Lua 5.1+. 27 | * Efficient. 28 | 29 | ## Usage 30 | 31 | Copy `lester.lua` file to a project and require it, 32 | which returns a table that includes all of the functionality: 33 | 34 | ```lua 35 | local lester = require 'lester' 36 | local describe, it, expect = lester.describe, lester.it, lester.expect 37 | 38 | -- Customize lester configuration. 39 | lester.show_traceback = false 40 | 41 | -- Parse arguments from command line. 42 | lester.parse_args() 43 | 44 | describe('my project', function() 45 | lester.before(function() 46 | -- This function is run before every test. 47 | end) 48 | 49 | describe('module1', function() -- Describe blocks can be nested. 50 | it('feature1', function() 51 | expect.equal('something', 'something') -- Pass. 52 | end) 53 | 54 | it('feature2', function() 55 | expect.truthy(false) -- Fail. 56 | end) 57 | 58 | local feature3_test_enabled = false 59 | it('feature3', function() -- This test will be skipped. 60 | expect.truthy(false) -- Fail. 61 | end, feature3_test_enabled) 62 | end) 63 | end) 64 | 65 | lester.report() -- Print overall statistic of the tests run. 66 | lester.exit() -- Exit with success if all tests passed. 67 | ``` 68 | 69 | ## Customizing output with environment variables 70 | 71 | To customize the output of lester externally, 72 | you can set the following environment variables before running a test suite: 73 | 74 | * `LESTER_QUIET="true"`, omit print of passed tests. 75 | * `LESTER_COLOR="false"`, disable colored output. 76 | * `LESTER_SHOW_TRACEBACK="false"`, disable traceback on test failures. 77 | * `LESTER_SHOW_ERROR="false"`, omit print of error description of failed tests. 78 | * `LESTER_STOP_ON_FAIL="true"`, stop on first test failure. 79 | * `LESTER_UTF8TERM="false"`, disable printing of UTF-8 characters. 80 | * `LESTER_FILTER="some text"`, filter the tests that should be run. 81 | 82 | Note that these configurations can be changed via script too, check the documentation. 83 | 84 | ## Customizing output with command line arguments 85 | 86 | You can also customize output using command line arguments 87 | if `lester.parse_args()` is called at startup. 88 | 89 | The following command line arguments are available: 90 | 91 | * `--quiet`, omit print of passed tests. 92 | * `--no-quiet`, show print of passed tests. 93 | * `--no-color`, disable colored output. 94 | * `--no-show-traceback`, disable traceback on test failures. 95 | * `--no-show-error`, omit print of error description of failed tests. 96 | * `--stop-on-fail`, stop on first test failure. 97 | * `--no-utf8term`, disable printing of UTF-8 characters. 98 | * `--filter="some text"`, filter the tests that should be run. 99 | 100 | ]] 101 | 102 | -- Returns whether the terminal supports UTF-8 characters. 103 | local function is_utf8term() 104 | local lang = os.getenv('LANG') 105 | return (lang and lang:lower():match('utf%-?8$')) and true or false 106 | end 107 | 108 | -- Returns whether a system environment variable is "true". 109 | local function getboolenv(varname, default) 110 | local val = os.getenv(varname) 111 | if val == 'true' then 112 | return true 113 | elseif val == 'false' then 114 | return false 115 | end 116 | return default 117 | end 118 | 119 | -- The lester module. 120 | local lester = { 121 | --- Whether lines of passed tests should not be printed. False by default. 122 | quiet = getboolenv('LESTER_QUIET', false), 123 | --- Whether the output should be colorized. True by default. 124 | color = getboolenv('LESTER_COLOR', true), 125 | --- Whether a traceback must be shown on test failures. True by default. 126 | show_traceback = getboolenv('LESTER_SHOW_TRACEBACK', true), 127 | --- Whether the error description of a test failure should be shown. True by default. 128 | show_error = getboolenv('LESTER_SHOW_ERROR', true), 129 | --- Whether test suite should exit on first test failure. False by default. 130 | stop_on_fail = getboolenv('LESTER_STOP_ON_FAIL', false), 131 | --- Whether we can print UTF-8 characters to the terminal. True by default when supported. 132 | utf8term = getboolenv('LESTER_UTF8TERM', is_utf8term()), 133 | --- A string with a lua pattern to filter tests. Nil by default. 134 | filter = os.getenv('LESTER_FILTER') or '', 135 | --- Function to retrieve time in seconds with milliseconds precision, `os.clock` by default. 136 | seconds = os.clock, 137 | } 138 | 139 | -- Variables used internally for the lester state. 140 | local lester_start = nil 141 | local last_succeeded = false 142 | local level = 0 143 | local successes = 0 144 | local total_successes = 0 145 | local failures = 0 146 | local total_failures = 0 147 | local skipped = 0 148 | local total_skipped = 0 149 | local start = 0 150 | local befores = {} 151 | local afters = {} 152 | local names = {} 153 | 154 | -- Color codes. 155 | local color_codes = { 156 | reset = string.char(27) .. '[0m', 157 | bright = string.char(27) .. '[1m', 158 | red = string.char(27) .. '[31m', 159 | green = string.char(27) .. '[32m', 160 | yellow = string.char(27) .. '[33m', 161 | blue = string.char(27) .. '[34m', 162 | magenta = string.char(27) .. '[35m', 163 | } 164 | 165 | local quiet_o_char = string.char(226, 151, 143) 166 | 167 | -- Colors table, returning proper color code if color mode is enabled. 168 | local colors = setmetatable({}, { __index = function(_, key) 169 | return lester.color and color_codes[key] or '' 170 | end}) 171 | 172 | --- Table of terminal colors codes, can be customized. 173 | lester.colors = colors 174 | 175 | -- Parse command line arguments from `arg` table. 176 | -- It `arg` is nil then the global `arg` is used. 177 | function lester.parse_args(arg) 178 | for _,opt in ipairs(arg or _G.arg) do 179 | local name, value 180 | if opt:find('^%-%-filter') then 181 | name = 'filter' 182 | value = opt:match('^%-%-filter%=(.*)$') 183 | elseif opt:find('^%-%-no%-[a-z0-9-]+$') then 184 | name = opt:match('^%-%-no%-([a-z0-9-]+)$'):gsub('-','_') 185 | value = false 186 | elseif opt:find('^%-%-[a-z0-9-]+$') then 187 | name = opt:match('^%-%-([a-z0-9-]+)$'):gsub('-','_') 188 | value = true 189 | end 190 | if value ~= nil and lester[name] ~= nil and (type(lester[name]) == 'boolean' or type(lester[name]) == 'string') then 191 | lester[name] = value 192 | end 193 | end 194 | end 195 | 196 | --- Describe a block of tests, which consists in a set of tests. 197 | -- Describes can be nested. 198 | -- @param name A string used to describe the block. 199 | -- @param func A function containing all the tests or other describes. 200 | function lester.describe(name, func) 201 | if level == 0 then -- Get start time for top level describe blocks. 202 | failures = 0 203 | successes = 0 204 | skipped = 0 205 | start = lester.seconds() 206 | if not lester_start then 207 | lester_start = start 208 | end 209 | end 210 | -- Setup describe block variables. 211 | level = level + 1 212 | names[level] = name 213 | -- Run the describe block. 214 | func() 215 | -- Cleanup describe block. 216 | afters[level] = nil 217 | befores[level] = nil 218 | names[level] = nil 219 | level = level - 1 220 | -- Pretty print statistics for top level describe block. 221 | if level == 0 and not lester.quiet and (successes > 0 or failures > 0) then 222 | local io_write = io.write 223 | local colors_reset, colors_green = colors.reset, colors.green 224 | io_write(failures == 0 and colors_green or colors.red, '[====] ', 225 | colors.magenta, name, colors_reset, ' | ', 226 | colors_green, successes, colors_reset, ' successes / ') 227 | if skipped > 0 then 228 | io_write(colors.yellow, skipped, colors_reset, ' skipped / ') 229 | end 230 | if failures > 0 then 231 | io_write(colors.red, failures, colors_reset, ' failures / ') 232 | end 233 | io_write(colors.bright, string.format('%.6f', lester.seconds() - start), colors_reset, ' seconds\n') 234 | end 235 | end 236 | 237 | -- Error handler used to get traceback for errors. 238 | local function xpcall_error_handler(err) 239 | return debug.traceback(tostring(err), 2) 240 | end 241 | 242 | -- Pretty print the line on the test file where an error happened. 243 | local function show_error_line(err) 244 | local info = debug.getinfo(3) 245 | local io_write = io.write 246 | local colors_reset = colors.reset 247 | local short_src, currentline = info.short_src, info.currentline 248 | io_write(' (', colors.blue, short_src, colors_reset, 249 | ':', colors.bright, currentline, colors_reset) 250 | if err and lester.show_traceback then 251 | local fnsrc = short_src..':'..currentline 252 | for cap1, cap2 in err:gmatch('\t[^\n:]+:(%d+): in function <([^>]+)>\n') do 253 | if cap2 == fnsrc then 254 | io_write('/', colors.bright, cap1, colors_reset) 255 | break 256 | end 257 | end 258 | end 259 | io_write(')') 260 | end 261 | 262 | -- Pretty print the test name, with breadcrumb for the describe blocks. 263 | local function show_test_name(name) 264 | local io_write = io.write 265 | local colors_reset = colors.reset 266 | for _,descname in ipairs(names) do 267 | io_write(colors.magenta, descname, colors_reset, ' | ') 268 | end 269 | io_write(colors.bright, name, colors_reset) 270 | end 271 | 272 | --- Declare a test, which consists of a set of assertions. 273 | -- @param name A name for the test. 274 | -- @param func The function containing all assertions. 275 | -- @param enabled If not nil and equals to false, the test will be skipped and this will be reported. 276 | function lester.it(name, func, enabled) 277 | -- Skip the test silently if it does not match the filter. 278 | if lester.filter then 279 | local fullname = table.concat(names, ' | ')..' | '..name 280 | if not fullname:match(lester.filter) then 281 | return 282 | end 283 | end 284 | local io_write = io.write 285 | local colors_reset = colors.reset 286 | -- Skip the test if it's disabled, while displaying a message 287 | if enabled == false then 288 | if not lester.quiet then 289 | io_write(colors.yellow, '[SKIP] ', colors_reset) 290 | show_test_name(name) 291 | io_write('\n') 292 | else -- Show just a character hinting that the test was skipped. 293 | local o = (lester.utf8term and lester.color) and quiet_o_char or 'o' 294 | io_write(colors.yellow, o, colors_reset) 295 | end 296 | skipped = skipped + 1 297 | total_skipped = total_skipped + 1 298 | return 299 | end 300 | -- Execute before handlers. 301 | for _,levelbefores in pairs(befores) do 302 | for _,beforefn in ipairs(levelbefores) do 303 | beforefn(name) 304 | end 305 | end 306 | -- Run the test, capturing errors if any. 307 | local success, err 308 | if lester.show_traceback then 309 | success, err = xpcall(func, xpcall_error_handler) 310 | else 311 | success, err = pcall(func) 312 | if not success and err then 313 | err = tostring(err) 314 | end 315 | end 316 | -- Count successes and failures. 317 | if success then 318 | successes = successes + 1 319 | total_successes = total_successes + 1 320 | else 321 | failures = failures + 1 322 | total_failures = total_failures + 1 323 | end 324 | -- Print the test run. 325 | if not lester.quiet then -- Show test status and complete test name. 326 | if success then 327 | io_write(colors.green, '[PASS] ', colors_reset) 328 | else 329 | io_write(colors.red, '[FAIL] ', colors_reset) 330 | end 331 | show_test_name(name) 332 | if not success then 333 | show_error_line(err) 334 | end 335 | io_write('\n') 336 | else 337 | if success then -- Show just a character hinting that the test succeeded. 338 | local o = (lester.utf8term and lester.color) and quiet_o_char or 'o' 339 | io_write(colors.green, o, colors_reset) 340 | else -- Show complete test name on failure. 341 | io_write(last_succeeded and '\n' or '', 342 | colors.red, '[FAIL] ', colors_reset) 343 | show_test_name(name) 344 | show_error_line(err) 345 | io_write('\n') 346 | end 347 | end 348 | -- Print error message, colorizing its output if possible. 349 | if err and lester.show_error then 350 | if lester.color then 351 | local errfile, errline, errmsg, rest = err:match('^([^:\n]+):(%d+): ([^\n]+)(.*)') 352 | if errfile and errline and errmsg and rest then 353 | io_write(colors.blue, errfile, colors_reset, 354 | ':', colors.bright, errline, colors_reset, ': ') 355 | if errmsg:match('^%w([^:]*)$') then 356 | io_write(colors.red, errmsg, colors_reset) 357 | else 358 | io_write(errmsg) 359 | end 360 | err = rest 361 | end 362 | end 363 | io_write(err, '\n\n') 364 | end 365 | io.flush() 366 | -- Stop on failure. 367 | if not success and lester.stop_on_fail then 368 | if lester.quiet then 369 | io_write('\n') 370 | io.flush() 371 | end 372 | lester.exit() 373 | end 374 | -- Execute after handlers. 375 | for _,levelafters in pairs(afters) do 376 | for _,afterfn in ipairs(levelafters) do 377 | afterfn(name) 378 | end 379 | end 380 | last_succeeded = success 381 | end 382 | 383 | --- Set a function that is called before every test inside a describe block. 384 | -- A single string containing the name of the test about to be run will be passed to `func`. 385 | function lester.before(func) 386 | local levelbefores = befores[level] 387 | if not levelbefores then 388 | levelbefores = {} 389 | befores[level] = levelbefores 390 | end 391 | levelbefores[#levelbefores+1] = func 392 | end 393 | 394 | --- Set a function that is called after every test inside a describe block. 395 | -- A single string containing the name of the test that was finished will be passed to `func`. 396 | -- The function is executed independently if the test passed or failed. 397 | function lester.after(func) 398 | local levelafters = afters[level] 399 | if not levelafters then 400 | levelafters = {} 401 | afters[level] = levelafters 402 | end 403 | levelafters[#levelafters+1] = func 404 | end 405 | 406 | --- Pretty print statistics of all test runs. 407 | -- With total success, total failures and run time in seconds. 408 | function lester.report() 409 | local now = lester.seconds() 410 | local colors_reset = colors.reset 411 | io.write(lester.quiet and '\n' or '', 412 | colors.green, total_successes, colors_reset, ' successes / ', 413 | colors.yellow, total_skipped, colors_reset, ' skipped / ', 414 | colors.red, total_failures, colors_reset, ' failures / ', 415 | colors.bright, string.format('%.6f', now - (lester_start or now)), colors_reset, ' seconds\n') 416 | io.flush() 417 | return total_failures == 0 418 | end 419 | 420 | --- Exit the application with success code if all tests passed, or failure code otherwise. 421 | function lester.exit() 422 | -- Collect garbage before exiting to call __gc handlers 423 | collectgarbage() 424 | collectgarbage() 425 | os.exit(total_failures == 0, true) 426 | end 427 | 428 | local expect = {} 429 | --- Expect module, containing utility function for doing assertions inside a test. 430 | lester.expect = expect 431 | 432 | --- Converts a value to a human-readable string. 433 | -- If the final string not contains only ASCII characters, 434 | -- then it is converted to a Lua hexdecimal string. 435 | function expect.tohumanstring(v) 436 | local s = tostring(v) 437 | if s:find'[^ -~\n\t]' then -- string contains non printable ASCII 438 | return '"'..s:gsub('.', function(c) return string.format('\\x%02X', c:byte()) end)..'"' 439 | end 440 | return s 441 | end 442 | 443 | --- Check if a function fails with an error. 444 | -- If `expected` is nil then any error is accepted. 445 | -- If `expected` is a string then we check if the error contains that string. 446 | -- If `expected` is anything else then we check if both are equal. 447 | function expect.fail(func, expected) 448 | local ok, err = pcall(func) 449 | if ok then 450 | error('expected function to fail', 2) 451 | elseif expected ~= nil then 452 | local found = expected == err 453 | if not found and type(expected) == 'string' then 454 | found = string.find(tostring(err), expected, 1, true) 455 | end 456 | if not found then 457 | error('expected function to fail\nexpected:\n'..tostring(expected)..'\ngot:\n'..tostring(err), 2) 458 | end 459 | end 460 | end 461 | 462 | --- Check if a function does not fail with a error. 463 | function expect.not_fail(func) 464 | local ok, err = pcall(func) 465 | if not ok then 466 | error('expected function to not fail\ngot error:\n'..expect.tohumanstring(err), 2) 467 | end 468 | end 469 | 470 | --- Check if a value is not `nil`. 471 | function expect.exist(v) 472 | if v == nil then 473 | error('expected value to exist\ngot:\n'..expect.tohumanstring(v), 2) 474 | end 475 | end 476 | 477 | --- Check if a value is `nil`. 478 | function expect.not_exist(v) 479 | if v ~= nil then 480 | error('expected value to not exist\ngot:\n'..expect.tohumanstring(v), 2) 481 | end 482 | end 483 | 484 | --- Check if an expression is evaluates to `true`. 485 | function expect.truthy(v) 486 | if not v then 487 | error('expected expression to be true\ngot:\n'..expect.tohumanstring(v), 2) 488 | end 489 | end 490 | 491 | --- Check if an expression is evaluates to `false`. 492 | function expect.falsy(v) 493 | if v then 494 | error('expected expression to be false\ngot:\n'..expect.tohumanstring(v), 2) 495 | end 496 | end 497 | 498 | --- Returns raw tostring result for a value. 499 | local function rawtostring(v) 500 | local mt = getmetatable(v) 501 | if mt then 502 | setmetatable(v, nil) 503 | end 504 | local s = tostring(v) 505 | if mt then 506 | setmetatable(v, mt) 507 | end 508 | return s 509 | end 510 | 511 | -- Returns key suffix for a string_eq table key. 512 | local function strict_eq_key_suffix(k) 513 | if type(k) == 'string' then 514 | if k:find('^[a-zA-Z_][a-zA-Z0-9]*$') then -- string is a lua field 515 | return '.'..k 516 | elseif k:find'[^ -~\n\t]' then -- string contains non printable ASCII 517 | return '["'..k:gsub('.', function(c) return string.format('\\x%02X', c:byte()) end)..'"]' 518 | else 519 | return '["'..k..'"]' 520 | end 521 | else 522 | return string.format('[%s]', rawtostring(k)) 523 | end 524 | end 525 | 526 | --- Compare if two values are equal, considering nested tables. 527 | function expect.strict_eq(t1, t2, name) 528 | if rawequal(t1, t2) then return true end 529 | name = name or 'value' 530 | local t1type, t2type = type(t1), type(t2) 531 | if t1type ~= t2type then 532 | return false, string.format("expected types to be equal for %s\nfirst: %s\nsecond: %s", 533 | name, t1type, t2type) 534 | end 535 | if t1type == 'table' then 536 | if getmetatable(t1) ~= getmetatable(t2) then 537 | return false, string.format("expected metatables to be equal for %s\nfirst: %s\nsecond: %s", 538 | name, expect.tohumanstring(t1), expect.tohumanstring(t2)) 539 | end 540 | for k,v1 in pairs(t1) do 541 | local ok, err = expect.strict_eq(v1, t2[k], name..strict_eq_key_suffix(k)) 542 | if not ok then 543 | return false, err 544 | end 545 | end 546 | for k,v2 in pairs(t2) do 547 | local ok, err = expect.strict_eq(v2, t1[k], name..strict_eq_key_suffix(k)) 548 | if not ok then 549 | return false, err 550 | end 551 | end 552 | elseif t1 ~= t2 then 553 | return false, string.format("expected values to be equal for %s\nfirst:\n%s\nsecond:\n%s", 554 | name, expect.tohumanstring(t1), expect.tohumanstring(t2)) 555 | end 556 | return true 557 | end 558 | 559 | --- Check if two values are equal. 560 | function expect.equal(v1, v2) 561 | local ok, err = expect.strict_eq(v1, v2) 562 | if not ok then 563 | error(err, 2) 564 | end 565 | end 566 | 567 | --- Check if two values are not equal. 568 | function expect.not_equal(v1, v2) 569 | if expect.strict_eq(v1, v2) then 570 | local v1s, v2s = expect.tohumanstring(v1), expect.tohumanstring(v2) 571 | error('expected values to be not equal\nfirst value:\n'..v1s..'\nsecond value:\n'..v2s, 2) 572 | end 573 | end 574 | 575 | return lester 576 | 577 | --[[ 578 | The MIT License (MIT) 579 | 580 | Copyright (c) 2021-2023 Eduardo Bart (https://github.com/edubart) 581 | 582 | Permission is hereby granted, free of charge, to any person obtaining a copy 583 | of this software and associated documentation files (the "Software"), to deal 584 | in the Software without restriction, including without limitation the rights 585 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 586 | copies of the Software, and to permit persons to whom the Software is 587 | furnished to do so, subject to the following conditions: 588 | 589 | The above copyright notice and this permission notice shall be included in all 590 | copies or substantial portions of the Software. 591 | 592 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 593 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 594 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 595 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 596 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 597 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 598 | SOFTWARE. 599 | ]] 600 | -------------------------------------------------------------------------------- /rockspecs/lester-0.1.5-1.rockspec: -------------------------------------------------------------------------------- 1 | package = "lester" 2 | version = "0.1.5-1" 3 | source = { 4 | url = "git://github.com/edubart/lester.git", 5 | tag = "v0.1.5" 6 | } 7 | description = { 8 | summary = "Minimal test framework for Lua", 9 | homepage = "https://github.com/edubart/lester", 10 | license = "MIT" 11 | } 12 | dependencies = { 13 | "lua >= 5.1", 14 | } 15 | build = { 16 | type = "builtin", 17 | modules = { 18 | ['lester'] = 'lester.lua', 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /rockspecs/lester-dev-1.rockspec: -------------------------------------------------------------------------------- 1 | package = "lester" 2 | version = "dev-1" 3 | source = { 4 | url = "git://github.com/edubart/lester.git", 5 | branch = "main" 6 | } 7 | description = { 8 | summary = "Minimal test framework for Lua", 9 | homepage = "https://github.com/edubart/lester", 10 | license = "MIT" 11 | } 12 | dependencies = { 13 | "lua >= 5.1", 14 | } 15 | build = { 16 | type = "builtin", 17 | modules = { 18 | ['lester'] = 'lester.lua', 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tests.lua: -------------------------------------------------------------------------------- 1 | local lester = require 'lester' 2 | 3 | local describe, it, expect = lester.describe, lester.it, lester.expect 4 | local skipfail = os.getenv('LESTER_TEST_SKIP_FAIL') == 'true' 5 | 6 | lester.parse_args() 7 | 8 | describe("lester", function() 9 | local b = false 10 | lester.before(function() 11 | b = true 12 | end) 13 | 14 | lester.after(function() 15 | b = false 16 | end) 17 | 18 | assert(not b) 19 | it("before and after", function() 20 | assert(b) 21 | end) 22 | assert(not b) 23 | 24 | it("assert", function() 25 | assert(true) 26 | end, true) 27 | 28 | it("skip", function() assert(false) end, false) 29 | 30 | it("expect", function() 31 | expect.fail(function() error() end) 32 | expect.fail(function() error'an error' end, 'an error') 33 | expect.fail(function() error'got my error' end, 'my error') 34 | expect.not_fail(function() end) 35 | expect.truthy(true) 36 | expect.falsy(false) 37 | expect.exist(1) 38 | expect.exist(false) 39 | expect.exist(true) 40 | expect.exist('') 41 | expect.not_exist(nil) 42 | expect.equal(1, 1) 43 | expect.equal(false, false) 44 | expect.equal(nil, nil) 45 | expect.equal(true, true) 46 | expect.equal('', '') 47 | expect.equal('asd', 'asd') 48 | expect.equal('\x01\x02', '\x01\x02') 49 | expect.equal({a=1}, {a=1}) 50 | expect.equal({a=1}, {a=1}) 51 | expect.not_equal('asd', 'asf') 52 | expect.not_equal('\x01\x02', '\x01\x03') 53 | expect.not_equal(1,2) 54 | expect.not_equal(true,false) 55 | expect.not_equal(nil,false) 56 | expect.not_equal({a=2},{a=1}) 57 | end) 58 | 59 | describe("fails", function() 60 | if not skipfail then 61 | it("empty error", function() 62 | error() 63 | end) 64 | it("string error", function() 65 | error 'an error' 66 | end) 67 | it("string error with lines", function() 68 | error '@somewhere:1: an error' 69 | end) 70 | it("table error", function() 71 | error {} 72 | end) 73 | 74 | it("empty assert", function() 75 | assert() 76 | end) 77 | it("assert false", function() 78 | assert(false) 79 | end) 80 | it("assert false with message", function() 81 | assert(false, 'my message') 82 | end) 83 | 84 | it("fail", function() 85 | expect.fail(function() end) 86 | end) 87 | it("fail message", function() 88 | expect.fail(function() error('error1') end, 'error2') 89 | end) 90 | it("not fail", function() 91 | expect.not_fail(function() error() end) 92 | end) 93 | 94 | it("exist", function() 95 | expect.exist(nil) 96 | end) 97 | it("not exist", function() 98 | expect.not_exist(true) 99 | end) 100 | 101 | it("truthy", function() 102 | expect.truthy() 103 | end) 104 | it("falsy", function() 105 | expect.falsy(1) 106 | end) 107 | 108 | it("equal", function() 109 | expect.equal(1, 2) 110 | end) 111 | it("not equal", function() 112 | expect.not_equal(1, 1) 113 | end) 114 | 115 | it("equal (table)", function() 116 | expect.equal({a=1}, {a=2}) 117 | end) 118 | 119 | it("not equal (table)", function() 120 | expect.not_equal({a=1}, {a=1}) 121 | end) 122 | 123 | it("equal (binary)", function() 124 | expect.equal('\x01\x02', '\x01\x03') 125 | end) 126 | 127 | it("not equal (binary)", function() 128 | expect.not_equal('\x01\x02', '\x01\x02') 129 | end) 130 | end 131 | end) 132 | end) 133 | 134 | lester.report() 135 | if skipfail then 136 | lester.exit() 137 | end 138 | --------------------------------------------------------------------------------