Package: python-property-based-testing
PyBTInstallationpipinstallpython-property-based-testingIntroductionYou can find a primer on property-based testinghere.PyBTis a library for property based testing in python. The main idea, is to state and randomly test properties about functions at large scale. The main functionalityPyBTprovides is a decorator namedpybt.The parameter takes the following arguments:n: the number of tests to run (defaults to 1000).generators: user provided generators to be used to generate function arguments (defaults to None).hypotheses: hypotheses that generated arguments are constrained by.max_basic_arg_size: maximum size of string, ints, floats etc.max_complex_arg_size: maximum size to use for complex structures like list an dict. Also applies to the depth of random types generated by any.Types SupportedPyBTsupports basic types, union types, and nested types. It supportslist,dict,int,str,float,bool. It also supportsany, which will generate a random type, and arguments of that type. The types generated byanyare constrained tolist,dict,int,str,float, andbool.There are plans to add random generation of classes and callables in the future. It is always possible to test functions with types not supported by PyBT. To do this, one needs only use custom generators. You can see an example below, in the sectionGenerators.Use and ExamplePyBTis used by decorating test cases with thepybtfunction. The key difference to other kinds of testing, is that the test defined only states apropertythat should hold for a function or program.PyBTwill then test this propertyntimes by generating random arguments matching the type annotations of your test. Let's run through an example.fromunittestimportTestCasefrompybt.core.coreimportpybtdefrev(l):returnl[::-1]classTestRevSimple(TestCase):pybt_small=pybt(max_complex_arg_size=5,max_basic_arg_size=1000)@pybt_smalldeftest_rev(self,l:list):assertrev(rev(l))==lHere we have a function namedrev, which reverses a list. We want to test this function using theunittestframework. We declare everything as we would with a normal unit test, with a few caveates. The most important distinction is that our test is a property over an abstracted listl. We are not specifying the actual test case by defining whatlmight be.First we importpybtfrom PyBT. We also import theTestCaseclass from the unittest framework. Then, we declare our test class, which inherits fromTestCase. This is common practice in python unit testing.Then, we declare a modified decoratorpybt_small, which sets the maximum basic type size to1000, and the maximum complex arg size to5. Finally, we declare the property we'd like to test in the methodtest_rev. We decorate this function withpybt_small. Notice that the type oflis a genericlist. This is equivalent to annotatinglaslist[any].We can now runtest_revthrough theunittestframework. When we do this,pybt_smallwill generaten = 1000random cases oflist[any]. If we want to see what the test cases are, we can simplyprint(l)withintest_rev.[201.71629512275953,True,True,False][True,-342.3693684184344][True,True,False][-467.5006903969188][558.8977953526747,150,263.0229074872361,-1.0862251449434799][67.66371838907641,-20.983597734832696,-55,False,-224.6313237684315][True,618.2506920564614][False,-168.82956631670407,True,True][266.1946254289064,-139.94823413505858,28,-260.6052834651554,False][False][True,96,-500.6663721540626,-458.2161805936233][22.251442861132976,2.7328417702831436,-239.33983760608413,-249,False]...Running this again, we see that both the generated lists and their types are different.[{'OqbXQRTbZjesxinNdgxwjkrNqCDSVbhXloFYgvNHjlDmjHglDFihmmMPgrnEzYgmMjKbvuNayXkIMlGxQWrEHquLvtiYpuJRriPmFzKuGgsZBUfSCPXTHNVQWdsqXFdfNkbdVZnVEvhYTnEanGOfONjGRssGpHAesEkPdblYTqZebfbbEsjtAYhBczhJLvEEvOfamgcjsyEsnNagDxCrmiEDKUlxgQcCourCnxxkqNDdfQdyWMtXjeUgUVIYAiTipPBVdrBrxnSBtBhrOwSeyZbmoovrqkMhRiZebi':'LqcgYSTdxAVEokqYnJMOBKiZAxIKdJBGQnIPtnJWrnIQshzjobsDikLBqJiIDvGoUCOKqbgTxqBhGcbwRanVNXBc','VlxxfsXmfRtkDLvEkMUeetRWNFmUhZTmXxNmsrFmKMJSWYKtgyyrFFxFVhZOPskXludESQCfbDGjiaItGpsbbMCCXhRrSYcKvdozuoKcnqcovPPawnJDWqZKxaNrfrLqypyAyNQUiKgHYDvqcDtFUMFFaRZMheGACyuACWJiCZUzjFlSIYhPvatsArIpAPgOoESeGkMBMdUwhsdLfGCYShNXapuuvmItPkQLjxkafwOeMECJcmaIgCxvPXTLHbdhQLSyaoeVbpfjHnfVFvQdaCPofARZLDlLYLQVzlhcivPiEXuOsRiLdmfvjpxBioccscYhnqASfsVYLQzkuKwDQeMNDcSBVeGBJAmiTHazSFqKtTlzYizVMJRfBWUGIrJYpIrsxsGcwaXUkdneUSvHgwiJEWqZFEhCUJpBTaakjrnwiJPmScrOTsOIZUIDAumnWyAkVMDRBSZeQAocgbvNyipFJMDAeJrRdRSFYPDRmwcbEBDlTKykexOGMtiOlLqlXWGlxPyaIOvyuuQrptxxxzwrhfPlfAIPscohiXvIapzhBQrwHCZPGtddIKpRTPunHuuJEVuRRHJbFYQYzfhGWxizJtAAEgYjPQFrSMuchvhifpURmizpFpUBQKolSakHUamKBIuzvNWrZqWgliJhoHwrQRYDTtsMVFEhUZJBWqqrrEqpH':'WJEVtzSUTSmRlHGbnJkuAzLDhizFnVRoeZeofrLZckypBJmMWQuRbWAXyEzSboXwPnkoPBLqILGuBwkYVQBOEiVvfxylpGaiPuFYpxnsPblCNnQqusHuEklSltNivYQUZEXnVZUvmkDcbZLWgqFlzziwJPWBVGovmQGAcGEYUUtsNSrtjIHQHIIbUNnAkigFedoynnLHZpVwoTSdkvDeDBzaQxyLtyzRqchxEiEqvVOttDrzfRXJsHDUVDKYtionmXXihsLeFpEpZYpwVEdivqCcMtRAZtQsWNQtcFBgeXteacnMgeHtuEnRJCWhlBwIgHZdRXDcajycfeuuZjvAxvRUmdPHyqVsJkDzyVUgogKqYhcUORwcjiKsAXxJxfYsdVhePqOnwtpFIYCnSCTNJVfQHzpxDYAglKsepxsKqsufscDWpOkrAmHXikCFtpoHeSwbkEEzrlvPAyNxnntcfEWQQxJxgvYkRxLDtCWNvjUXvBZRqOlQIWGPLgPApaIxtNjsqWnuzsymHXtxYJnOxkJzgIyZnpkzdGrflJmncnPEeOhlgpUKnkASSuRnocvHNOMCnyJGhkWybKZisfMXcfiCysUKyxJEuqTOEfkzgWSHdyp','UUjaAnpquicDjbeqKWghUjJqvXQlWgeSwDZfvJwefygaRfouOCMzbBysRwSAjNKsWjhYjTdtsXjYEROVaTypRgtKblnLhdYBVMtvQAnMgEApoHnqZFRqCzRPOAMetgWoGimhWHhxSYVbCWrDkWxiJPtUEyC':-338.5739091864339}][176,{101:-245},-899,{496:[{},[-763,False,51.61292833180636,-704.6588436548865]],657:-220,-36:496,-870:[{'mHeJJRaBYYIgzivUkaGhCciYICrHuCDUCLvwmGGhHetxwhzdzpHskZNXwuqJwEiuhJfjuFJNCdNclseiVUNteLvvUKsIBhQqgwySkkiDebsGfmBEiZZdXWmYMqRMXHqeHtOeMjyAgjOeLOBIklpYTjMoWWWWBE':694.309135177688},True,False,[515,-132.03329321479674],True],129:672},{287:'pztxwzqtxGwmXaydOBfScIczLFbTSXxHEYMCQTLIiIXeLbBzXllgHJqzOSJRbPqPXVZxwhbwQYdjxPqUmvwaRxfSWqlBCJQPBaLSJoWdAfVkJyyyXQLaeOZAQRhjMairmGiuzvAlzJxCqfFREOsgguowxtsmldbdYBQZbumdXkzLLfnCgvdqaFvEchoKiIIrJqZaByTNRHvMMyzNupHYjDdEocdUcBikQycuurOUGfXFRNjPPjEzTuYTkoCdWjxkjgMJPyLfgwNddOyeJglITnZmsZJzzEVPzmSrDmHAzGSkjYoNCQStyespkUtwPPDzFrCziQIxQLFhvtxjpYZUyIOHzWKcTGvEPaeyHWtoisieNrGgYinewuAYMzsCPbRjBydVLoZtkIZEgdtvHzibbKrCKxeBSBjSSMDQgzgMLLwVmpWIqESlgiqnToqIROFQvgZRHBemwdggcSSVABBqFuViQynQGuxMsrJLqokLRxCGtLKoXYuMFzFqumzjhNAeYwJLSZgGmoezGsxabNfAuknkKNqfIU',77:[[-13.487063433086522,-85.48259578583449]]}]...As we can see, our second run generated much more complex lists than our first. Both the scale and complexity of the arguments generated give us strong guarantees that the property we enforced for our functionrevholds.Advanced UseHypothesesIf you need to constrain the arguments that are passed to your function, you can constrain them usinghypotheses. For example, if we wanted every single integer that appeared inlto be less than2forrev, we would specify that in the following way:defconstrain_l(l:list):forelinl:iftype(el)==int:ifel>2:returnFalsereturnTruehypotheses={"l":lambdal:constrain_l(l)}pybt_small=pybt(max_complex_arg_size=5,max_basic_arg_size=1000,hypotheses=hypotheses)GeneratorsIf you have arguments that should be generated in a very specific way, then you can provide your own generators, through thegeneratorsargument. For example, let's see we only wanted to our rev function on lists of ints that are coerced to strings. We could do the following.importrandomdefgen_l():l=[]for_inrange(5):el=random.randint(1000)l.append(str(el))returnlgenerators={"l":gen_l}pybt_small=pybt(max_complex_arg_size=5,max_basic_arg_size=1000,generators=generators)