{"id":16087133,"url":"https://github.com/dionisiydk/statespecs","last_synced_at":"2025-03-18T06:30:44.376Z","repository":{"id":22786136,"uuid":"97141971","full_name":"dionisiydk/StateSpecs","owner":"dionisiydk","description":"Object validation framework based on first class specs","archived":false,"fork":false,"pushed_at":"2022-12-21T22:48:18.000Z","size":286,"stargazers_count":6,"open_issues_count":8,"forks_count":7,"subscribers_count":3,"default_branch":"master","last_synced_at":"2024-02-10T06:10:32.973Z","etag":null,"topics":["bdd","pharo","sspec","statespecs","sunit","tdd"],"latest_commit_sha":null,"homepage":null,"language":"Smalltalk","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/dionisiydk.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2017-07-13T16:06:07.000Z","updated_at":"2023-06-08T02:53:23.000Z","dependencies_parsed_at":"2023-01-13T22:13:13.051Z","dependency_job_id":null,"html_url":"https://github.com/dionisiydk/StateSpecs","commit_stats":null,"previous_names":[],"tags_count":14,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dionisiydk%2FStateSpecs","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dionisiydk%2FStateSpecs/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dionisiydk%2FStateSpecs/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dionisiydk%2FStateSpecs/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/dionisiydk","download_url":"https://codeload.github.com/dionisiydk/StateSpecs/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":243906423,"owners_count":20367027,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["bdd","pharo","sspec","statespecs","sunit","tdd"],"created_at":"2024-10-09T13:28:03.695Z","updated_at":"2025-03-18T06:30:43.974Z","avatar_url":"https://github.com/dionisiydk.png","language":"Smalltalk","funding_links":[],"categories":[],"sub_categories":[],"readme":"# StateSpecs\n\n[![GitHub release](https://img.shields.io/github/release/dionisiydk/StateSpecs.svg)](https://github.com/dionisiydk/StateSpecs/releases/latest)\n[![Unit Tests](https://github.com/dionisiydk/StateSpecs/actions/workflows/tests.yml/badge.svg)](https://github.com/dionisiydk/StateSpecs/actions/workflows/tests.yml)\n\n[![Pharo 7.0](https://img.shields.io/badge/Pharo-7.0-informational)](https://pharo.org)\n[![Pharo 8.0](https://img.shields.io/badge/Pharo-8.0-informational)](https://pharo.org)\n[![Pharo 9.0](https://img.shields.io/badge/Pharo-9.0-informational)](https://pharo.org)\n[![Pharo 10](https://img.shields.io/badge/Pharo-10-informational)](https://pharo.org)\n[![Pharo 11](https://img.shields.io/badge/Pharo-11-informational)](https://pharo.org)\n\nStateSpecs is object state specification framework. It describes particular object states by first class specifications.\nFor example there are SpecOfCollectionItem, SpecOfObjectClass and SpecOfObjectSuperclass.\nStateSpecs provides fluent DSL to validate objects over these specification.\n\n## Installation\nUse following script for Pharo version \u003e= 6:\n```Smalltalk\nMetacello new\n  baseline: 'StateSpecs';\n  repository: 'github://dionisiydk/StateSpecs';\n  load\n```\nTo add dependency in your project baseline:\n```Smalltalk\nspec\n    baseline: 'StateSpecs'\n    with: [ spec repository: 'github://dionisiydk/StateSpecs:versionTagOrBranch' ]\n```\nFor old Pharo versions project should be loaded from smalltalkhub:\n```Smalltalk\nMetacello new\n      smalltalkhubUser: 'dionisiy' project: 'StateSpecs';\n      configuration: 'StateSpecs';\n      version: #stable;\n      load.\n```\n## Basic\n\nSpecifications can match and validate objects. In case when object does not satisfied a specification you will get failure result with detailed information about the problem.\nFor example try create SpecOfObjectClass and validate objects over it.\n\n```Smalltalk\nspec := SpecOfObjectClass requiredClass: SmallInteger.\nspec validate: 10. \"==\u003e a SpecOfValidationSuccess\"\nspec validate: 'some string'. \"==\u003e a SpecOfValidationFailure(Got 'some string' but it should be an instance of SmallInteger)\"\n```\n\nInstead of validation you can simply match objects for specification to get true or false:\n\n```Smalltalk\nspec matches: 10. \"==\u003e true\"\nspec matches: \"string\". \"==\u003e false\"\n```\n\nSpecifications can be inverted by *#not* message:\n\n```Smalltalk\nspec not validate: 10.  \"==\u003e a SpecOfValidationFailure(Got 10 but it should not be an instance of SmallInteger)\"\nspec not validate: 'some string'. \"==\u003e a SpecOfValidationSuccess\"\n```\n\n*#not* creates new spec instance. You can also invert current one with message *#invert*.\n\n\nTo easily create specifications and validate objects by them StateSpecs provides two kind DSL: should expressions and \"word\" classes.\n\nFirst allows you to write \"assertions\":\n\n```Smalltalk\n1 should be: 2\n1 should equal: 10\n```\n\nwhich create particular kind of specification and verify receiver over it.\n\n\nAnd second allows you to instantiate specs by natural readable words:\n\n```Smalltalk\nKind of: Number. \"==\u003e a SpecOfObjectSuperclass(should be a kind of Number)\"\nInstance of: String. \"==\u003e a SpecOfObjectClass(should be an instance of String)\"\nEqual to: 'test'. \"==\u003e a SpecOfEquality(should equal 'test')\"\n```\n\nWord classes were introduced to get fluent interface for mock expectations (mock stub someMessage: (Kind of: String)).\n\nBut they are very handy shorcuts to access specifications in general. Same word can return different specifications in different expressions which allows very fluent instantiation interface:\n\n```Smalltalk\nEqual to: 'test'. \"==\u003e a SpecOfEquality(should equal 'test')\"\nEqual to: 10.0123 within: 0.01. \"==\u003e  a SpecOfApproxEquality(should be within 0.01 of 10.0123)\"\n```\n\n## Should expressions\n\nUnderhood should expression creates particular kind of specification and verify receiver over it.\n\nWhen object is not valid should expression signals SpecOfFailed error. Then in debugger you can inspect validation result to deeply analyze the reason.\n\nSending extra *#not* message after *should* inverts logic of following expression:\n\n```Smalltalk\n3 should not equal: 3. \"fail with message: Got '3' but it should not equal '3'\"\n```\n\nShould expressions were created with the goal to replace SUnit assertions (self assert: a = b).\nThey were originally invented by Dave Astels in project SSpec as part of general rethinking of TDD methodology in flavor of BDD.\nNowdays SSpec approach was ported to many languages (NSpec in C#, RSpec in Ruby for example).\n\nStateSpecs provides clean implementation for should expressions on top of first class specifications. It uses more pragmatic approach for \"should syntax\" to avoid magic over expressions to make them more explorable with standart development tools.\n\nTo explore complete set of expressions look at SpecOfShouldExpression. It is also place where to extend them. SpecOfShouldExpressionTests describes them in tests.\n\n### Specification of object identity\n\n*#be:* message is used to verify that receiver is identical to given argument:\n\n```Smalltalk\n1 should be: 2.\n```\n\nIt fails with message: Got \"1\" but it should be \"2\".\n\n```Smalltalk\n1 should not be: 1.\n```\n\nIt fails with message: Got \"1\" but it should not be \"1\".\n\n### Specification of object equality\n\n*#equal:* message is used to verify that receiver is equivalent to given argument:\n\n```Smalltalk\n3 should equal: 2.\n```\n\nIt fails with message: Got \"3\" but it should equal \"2\".\n\n```Smalltalk\n3 should not equal: 3.\n```\n\nIt fails with message: Got \"3\" but it should not equal \"3\"\n\n\nLanguage equality operation #= is redefined by many classes according to domain logic. Sometimes they check a lot of conditions to compare objects.\nProblem that it can be not suitable from the point of view of specification. Imaging that we want compare two collection of different type:\n\n```Smalltalk\n#(1 2 3) asOrderedCollection = #(1 2 3). \"==\u003efalse\"\n```\n\nIt returns false which is correct from the point of view of collection library. But what we would expect from specification?\n\n```Smalltalk\n#(1 2 3) asOrderedCollection should equal: #(1 2 3).\n```\n\nIt would be not suitable to fail because it will force us to always think about collection type when we would like assert their equality.\nIn fact we are supposed to assert collection items with this expression and not instances of collections.\n\n\nSo this expression will not fail in StateSpecs. And to achieve it equality specification uses specific message *#checkStateSpecsEqualityTo:* instead of standart #=.\nDefault Object implementation calls #=. But some classes redefine it with appropriate logic to provide as less restrictive behaviour as possible.\n\nIdea is that general equality specification should be as much simple equality as possible with enough restrictions. And if you want some extra details you should use different explicit specification which describes them.\nIn case of collections you should check for collection class explicitly if it is important for your business case where you use specification:\n\n```Smalltalk\nactual := #(1 2 3) asOrderedCollection.\nexpected := #(1 2 3).\n\nactual should beInstanceOf: expected class.\nactual should equal: expected.\n```\n\nFollowing this logic StateSpecs do not check order when compare basic collection classes:\n\n```Smalltalk\n#(1 2 3) should equal: #(2 1 3). \"will not fail\"\n#(1 2 3) asSet should equal: #(2 1 3). \"will not fail\"\n```\n\nWhen order is important use different message *#equalInOrder:*:\n\n```Smalltalk\n#(1 2 3) asOrderedCollection should equalInOrder: #(2 1 3).\n```\n\nIt fails with message: Got \"#(1 2 3)\" but it should equal in order to \"#(2 1 3)\".\n\n\nThere are collection classes like String or ByteArray which are supposed to be in order and which type is important. For them theses properties are always taken into account for equality comparison:\n\n```Smalltalk\n'123' should equal: #($1 $2 $3).\n```\n\nIt fails with message: Got '123' but it should equal \"#($1 $2 $3)\".\n\n```Smalltalk\n'123' should equal: '132'.\n```\n\nIt fails with message: Got '123' but it should equal '132'.\n\n\nFloats are another example where specification behaves differently then standart language comparison:\n\n```Smalltalk\n0.1 + 0.2 = 0.3 \"==\u003e false\"\n```\n\nIt is correct result because of rounding errors in float arithmetics. But it is completelly not suitable to be part of specification. So in StateSpecs following expression will succeed:\n\n```Smalltalk\n(0.1 + 0.2) should equal: 0.3  \"==\u003e will not fail\"\n```\n\nFloat implements *#checkStateSpecsEqualityTo:* by comparing numbers with default accuracy.\n\n\nAnd there is special specification for floats when concrete accuracy is important:\n\n```Smalltalk\n10.123 should equal: 10.1 within: 0.1  \"==\u003e will not fail\"\n10.123 should equal: 10.1 within: 0.01 \"==\u003e will fail\"\n```\n\nLast expression fails with message: Got 10.123 but it should be within 0.01 of 10.1.\n\n\nSame logic is used by equality specification of Point class.\n\n### Specification of class relationship\n\n```Smalltalk\n3 should beKindOf: String.\n```\n\nIt will fail with message: Got 3 but it should be a kind of String.\n\n```Smalltalk\n3 should beInstanceOf: String.\n```\n\nIt will fail with message: Got 3 but it should be an instance of String.\n\n### Specifications of collection\n\nTo specify size of expected collection use *#haveSize:* message:\n\n```Smalltalk\n#(1 2) should haveSize: 10.\n```\n\nIt fails with message: Got #(1 2) but it should have 10 elements.\n\n\nThere is simple expression for empty collections:\n\n```Smalltalk\n#(1 2) should be isEmpty\n```\n\nIt fails with message: #(1 2) should be isEmpty. It uses predicate syntax explained below at *@predicate*.\n\n\nTo require concrete item in collection use one of *#include:* messages:\n\n```Smalltalk\n#(1 2) should include: 10.\n```\n\nIf fails with message: Got #(1 2) but it should include 10.\n\n```Smalltalk\n#(1 2) should include: 10 at: 1.\n```\n\nIt fails with message: Got 1 at key 1 of #(1 2) but should equal 10.\n\n\nArgument of include messages can be specification itself:\n\n```Smalltalk\n#(1 2) should include: (Instance of: String) at: 1.\n```\n\nIt fails with message: Got 1 at key 1 of #(1 2) but should be an instance of String.\n\n```Smalltalk\n#(1 2) should include: (Instance of: String)\n```\n\nIt fails with message: Got #(1 2) but should include (be an instance of String).\n\n```Smalltalk\n#(1 2) should include: [:each | each \u003e 1]\n```\n\nIt is succeed without error.\n\n\nTo specify expected key in dictionary use *#includeKey:* message:\n\n```Smalltalk\n{ #key1 -\u003e #value1 } asDictionary should includeKey: #key2\n```\n\nIt fails with message: Got a Dictionary{#key1-\u003e#value1} but it should include key #key2\n\n### Specifications of string\n\nTo specify the substring of expected string use ==includeSubstring:== message:\n\n```Smalltalk\n'some test string' should includeSubstring: 'test2'\n```\n\nIt fails with message: Got 'some test string' but it should include 'test2'.\n\n\nTo specify prefix of expected string use ==beginWith:== message:\n\n```Smalltalk\n'string for test' should beginWith: 'test'\n```\n\nIt fails with message: Got 'string for test' but it should begin with 'test'.\n\n\nTo specify suffix of expected string use ==endWith:== message:\n\n```Smalltalk\n'test string' should endWith: 'test'\n```\n\nIt fails with message: Got 'test string' but it should end with 'test'.\n\n\nTo specify regex expression which expected string should satisfy use ==matchRegex:== message:\n\n```Smalltalk\n'string for test' should matchRegex: '^test'\n```\n\nIt fails with message: Got 'string for test' but it should match regex '^test'.\n\n\nBy default all this specifications validate strings ignoring case.\nIf you want case sensitive specs just add ==caseSensitive: true== keyword to all examples:\n\n```Smalltalk\n'some test string' should includeSubstring: 'Test' caseSensitive: true\n'test string' should beginWith: 'Test' caseSensitive: true\n'string for test' should endWith: 'Test' caseSensitive: true\n'test string' should matchRegex: '^Test' caseSensitive: true\n```\n\n### Raising exception\n\n*#raise:* message allows specify expected failure of given block:\n\n```Smalltalk\n[1 + 2] should raise: ZeroDivide.\n```\n\nIt fails with message: Got no failures but should be an instance of ZeroDivide.\n\n```Smalltalk\n[1/0] should not raise: ZeroDivide.\n```\n\nIt fails with message: Got ZeroDivide but it should not be an instance of ZeroDivide.\n\n```Smalltalk\n[1/0] should raise: Error.\n```\n\nIt fails with message: Got ZeroDivide but it should be an instance of Error.\n\n```Smalltalk\n[1/0] should raise: (Kind of: Error).\n```\n\nIt not fails because ZeroDevide is kind of Error class.\n\nYou can use instance of expected exception instead of class:\n\n```Smalltalk\nerrorInstance := Error new messageText: 'test error'.\n[error signal] should raise: errorInstance\n```\n\nIt not fails.\n\n```Smalltalk\n[self error: 'another error'] should raise: errorInstance\n```\n\nIt fails with message: Got \"Error: another error\" but it should equal \"Error: test error\".\n\n\nAlso there is simple message *#fail* to expect general failure:\n\n```Smalltalk\n[1/0] should fail.\n```\n\nIt not fails beause block is really failed as expected.\n\n```Smalltalk\n[1/0] should not fail.\n```\n\nIt fails with message: Got ZeroDivide but it should not be a kind of Error.\n\n```Smalltalk\n[1+2] should fail.\n```\n\nIt fails with message: Got no failures but should be a kind of Error.\n\n### Predicate syntax\n@predicate\n\nIn many cases only thing which we want to specify is some boolean state of objects using their own methods.\nFor this porpose special SpecOfBooleanProperty specification is implemented which should be created with given boolean message:\n\n```Smalltalk\nspec := SpecOfBooleanProperty fromMessage: (Message selector: #isEmpty)\nspec validate: #()\n```\n\nMessage can includes arguments:\n\n```Smalltalk\nspec := SpecOfBooleanProperty fromMessage: (Message selector: #between:and: arguments: #(1 10))\nspec validate: 5\n```\n\nTo use this spec from should expression special *#be* message is introduced. Any following expression after #be will create message for boolean spec as in example above. And then it will validate #should receiver:\n\n```Smalltalk\n3 should be even.\n2 should not be even.\n```\n\nLast expression fails with message: 2 should not be even. Predicate expressions are always report problem as they were written.\n\nOther examples:\n\n```Smalltalk\n3 should be between: 10 and: 50.\n2 should not between: 1 and: 5.\n\n#(1 2) should be isEmpty.\n#() should not be isEmpty.\n```\n\n### Validation object properties\n\nLast interesting feature of StateSpecs is ability to validate particular property of object without loosing property itself.\nImaging that we want to validate x coordinate of rectangle origin. It could be done like this:\n\n```Smalltalk\n(1@3 corner: 20@1) origin x should equal: 100\n```\n\nIt fails with message: Got 1 but it should equal 100. But it has no information about what exact property of rectangle is wrong. Users would like to see it in failure message.\n\nTo achieve this goal StateSpecs introduced *#where* message which should be sent to receiver and all following messages up to *#should* will be recorded as object propety. At the end #should expression will validate retrieved property instead of receiver:\n\n```Smalltalk\n(1@3 corner: 20@30) where origin x should equal: 100.\n```\n\nIt fails with message: Got 1 from (1@1) corner: (20@3) origin x but it should equal 100.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdionisiydk%2Fstatespecs","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdionisiydk%2Fstatespecs","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdionisiydk%2Fstatespecs/lists"}