ios 对属性重构_准备通过属性测试进行重构

ios 对属性重构

TL / DR (TL/DR)

If you can think in properties, with or without a property testing library, your tests will support refactoring rather than being a barrier to it.

如果您可以考虑属性,无论是否具有属性测试库,您的测试都将支持重构,而不是阻碍重构。

I didn’t figure out what this post is about until I finished it, but here it is: This post is about why and how Unit Testing can be broken in your application. It’s about the difference between good DocTests and good Unit Tests. And ultimately, it’s about thinking in properties. We take a while to get there, and there are some detours, but ultimately, that’s where we’ll land.

在完成这篇文章之前,我没有弄清楚这是什么意思,但是这里是:这篇文章是关于为什么以及如何在您的应用程序中破坏单元测试的。 这是关于好的DocTests和好的单元测试之间的区别。 最终,这是关于属性的思考。 我们花了一些时间才能到达那里,并且有一些弯路,但是最终,这就是我们要降落的地方。

So a bit ago I wrote about the partiphify method, a part of the morphix library for Elixir. My conclusion was that I wasn’t particularly happy about the code I originally wrote for that method. It seemed clumsy. So I wanted to refactor it.

所以不久前,我写了关于partiphify方法的信息,该方法是Elixir的morphix库的一部分。 我的结论是,我对最初为该方法编写的代码并不特别满意。 看起来笨拙。 所以我想重构它。

But that led to another problem, which is example code, and unit testing. What the partiphify! method does is to take a list and divide it up into a fixed number of sublists of equal length, or, as equal length as possible. Given a list like [1, 2,3,4,5,6,7] and the integer 3, partiphify! should return three lists, which might be [[1,2,3], [4,5], [6,7]], but not [[1,2,3],[4,5,6],[7]]. The second partitioning doesn’t keep the lists as close in size as possible, but the first partitioning does.

但这导致了另一个问题,即示例代码和单元测试。 参加partiphify! 方法要做的是获取一个列表并将其分成固定数量的等长或尽可能长的子列表。 给定一个像[1, 2,3,4,5,6,7]和整数3 ,请partiphify! 应该返回三个列表,它们可能是[[1,2,3], [4,5], [6,7]] ,而不是[[1,2,3],[4,5,6],[7]] 。 第二个分区不会使列表的大小尽可能接近,但是第一个分区可以。

The thing about writing example code and unit tests for this kind of a method is that implementation matters. Here’s an example from the DocTests:

为这种方法编写示例代码和单元测试的事情是实现很重要。 这是来自DocTests的示例:

iex> Morphix.partiphify!([:a, :b, :c, :d, :e], 4)
[[:b], [:c], [:d], [:e, :a]]

Any implementation that ordered the list differently (for example [[:a,:b], [:c],[:d],[e]], would fail that unit test, even though the implementation might be correct in all the ways that matter. Which is okay in a doctest, which is more of an example than a test, but not great in a unit test.

任何将列表排序不同的实现(例如[[:a,:b], [:c],[:d],[e]]都会使该单元测试失败,即使该实现在所有在doctest中还可以,这更多的是示例而不是测试,但是在单元测试中效果不佳。

Which made this a great time to add property tests to the library. A property test is a test where the developer determines things that are true about the code (these are called invariants) and example inputs are generated by the testing code and tested against these invariants.

这是将属性测试添加到库的绝佳时机。 属性测试是一种测试,开发人员在该测试中确定代码的正确内容(这些称为不变式 ),并且示例输入由测试代码生成并针对这些不变式进行测试。

So the first task involved in writing a property test is to figure out what your invariants are. In the case of partiphify!, we know that the function should take two arguments, a list and an integer, and it should divide the list into k sublists that are as close in size to each other possible.

因此,编写属性测试所涉及的第一项任务是弄清您的不变式是什么。 在参与的情况下partiphify! ,我们知道该函数应该接受两个参数,一个列表和一个整数,并且应该将列表分为k个子列表,每个子列表的大小尽可能地接近。

Thinking about it a bit, we (hat tip to Jessica Kerr) came up with a list of 4 properties that make up the partiphify! function, given that there is an input list l, an input integer k, and an output value out.

仔细考虑一下,我们( Jessica Kerr的技巧提示)提出了构成参与性的4个属性的列表partiphify! 假设有一个输入列表l ,一个输入整数k和一个输出值out

  1. The length of out should be equal to k.

    out的长度应等于k

  2. Every item in l should be in out.

    l每一项都应该out

  3. The sum of the lengths of the items in out should equal the length of l.

    输入项out的长度之和应等于l的长度。

  4. The length of each list in out should be at most +-1 of any other list in out.

    每个列表的长度out最多应为+-1任何其他列表中out

And that’s it. If those properties match, then we know that we have divided our input list into partitions correctly. All the items are in the output, no item is in more than one sublist, we have the right number of sublists, and the sublists are balanced.

就是这样。 如果这些属性匹配,那么我们知道我们已将输入列表正确划分为多个分区。 所有项目都在输出中,没有一个项目在一个以上的子列表中,我们拥有正确数量的子列表,并且子列表是平衡的。

I wrote property tests, initially, using PropCheck, and then later rewrote those same tests using StreamData. PropCheck is a property testing library for Elixir that provides a wrapper around PropER, a property based testing library for Erlang.

我最初使用PropCheck编写了属性测试,然后使用StreamData重写了相同的测试。 PropCheck是Elixir的属性测试库,它为PropER提供包装, PropER是Erlang的基于属性的测试库。

To set up either library, you first want to add the library to your deps(), then add the application to your extra_applications: [] or applications: [] Keyword lists. I wanted to make sure these only ran in test, so for my application the changes to my mix.exs looked like this:

要设置这两个库,您首先要将该库添加到deps() ,然后将该应用程序添加到extra_applications: []applications: []关键字列表中。 我想确保它们仅在测试中运行,因此对于我的应用程序,对mix.exs的更改如下所示:

def application do
[applications: applications(Mix.env())]enddefp applications(:test) do
applications(:default) ++ [:stream_data, :propcheck]
enddefp applications(_) do
[:logger]enddefp deps do
[
{:excoveralls, "~> 0.8", only: [:dev, :test]},
{:ex_doc, "~> 0.18", only: :dev},
{:credo, "~> 0.9.1", only: [:dev, :test]},
{:stream_data, "~> 0.5.0", only: [:dev, :test]},
{:propcheck, "~> 1.1", only: [:dev, :test]}
]end

I have the changes for adding both StreamData and PropCheck here, but really you only need one of them.

我在此处添加了StreamData和PropCheck进行了更改,但实际上您只需要其中之一。

In your test itself, you’ll want to add the dependencies:

在测试本身中,您将要添加依赖项:

defmodule PartiphifyPropTest do
use ExUnit.Case use PropCheck

Or:

要么:

defmodule PartiphificationTest do
use ExUnit.Case use ExUnitProperties

So for our first property, this is the test I came up in PropCheck:

因此,对于我们的第一个属性,这是我在PropCheck中进行的测试:

# number of partitions should be correct
property "number of partitions is correct" do
forall {list, p} <- {list(), integer(1, 10)} do
partitioned = Morphix.partiphify!(list, p)
Enum.count(partitioned) == p
end
end

The property block acts like a test statement. The first line is the forall statement, which names two variables, list and p, and assigns generators to those variables. The first generator is list(), which will produce lists of any valid term, and integer(1,10), which will produce integers in the range of 1 to 10. The general form of this statement is

property块的作用类似于test语句。 第一行是forall语句,它命名两个变量listp ,并将生成器分配给这些变量。 第一个生成器是list() ,它将生成任何有效项的列表,以及integer(1,10) ,它将生成1到10范围内的整数。该语句的一般形式是

forall {var_1, var_2, …, var_n} <- {generator_1, generator_2, ..., generator_n}
....
end

Inside the forall statement, we want to have code that results in either a true or a false value. In this case, once we’ve partitioned the input list, we want to see that the number of partitions is equal to the input integer.

forall语句中,我们希望有导致结果为true或false的代码。 在这种情况下,对输入列表进行分区后,我们希望看到分区数等于输入整数。

In StreamData, the test looks like this:

在StreamData中,测试如下所示:

property "number of partitions is correct" do
check all list <- list_of(term(), min_length: 0, max_length: 100),
part <- integer(1..27) do
assert list
|> Morphix.partiphify!(part)
|> Enum.count()
|> Kernel.==(part)
end
end

So, again, we have a property statement, and inside that statement we have a check all statement instead of a forall statement. The check all statement adds variables one at a time rather than in tuples. So in this case, the first variable will be list, which is generated by list_of(term(), [options]), so will be lists of between 0 and 100 of type term() , and part, which is generated by integer(1..27). The general form of this statement is:

因此,同样,我们有一个property语句,在该语句内部,我们有一个check all语句而不是forall语句。 check all语句一次添加一个变量,而不是在元组中。 因此,在这种情况下,第一个变量将是list ,它是由list_of(term(), [options]) ,因此,将是0到100之间的term()类型的列表,而part是由integer(1..27)生成的integer(1..27) 。 该语句的一般形式为:

 check all var_1 <- generator_1,
var_2 <- generator_2,
...,
var_n <- generator_n do
...
end

Inside the check all statement, we use assert to test the property. The code here looks a little different because I’m using a pipechain, but all the same code is being called in the same order as in the PropCheck example.

check all语句中,我们使用assert来测试属性。 这里的代码看起来有些不同,因为我使用的是管道链,但是所有相同的代码都以与PropCheck示例中相同的顺序被调用。

Our second invariant is that every item that is in the original list is in the partitions, somewhere. For this property, the generators are the same, but the code that checks the invariant is fairly complex and might be worth looking at:

我们的第二个不变式是原始列表中的每个项目都在分区中的某个位置。 对于此属性,生成器是相同的,但是检查不变式的代码相当复杂,可能值得一看:

assert Enum.reduce(list, true, fn list_item, acc_a ->
in_a_list =
Enum.reduce(partitioned, false, fn partition, acc_b ->
Enum.member?(partition, list_item) || acc_b
end) in_a_list && acc_a
end)

Warning: In this next section I go pretty deep in the reduction logic for this invariant. I’m doing this mostly because using Enum.reduce/3 is not always intuitive for me, and I found it initially hard to wrap my head around it for uses other than adding up numbers. If you find the invariant code blindingly obvious, feel free to skip the next few paragraphs.

警告: 在下一部分中,我将深入介绍该不变式的归约逻辑。 我这样做主要是因为使用 Enum.reduce/3 对我来说并不总是直观的,而且我发现最初很难将其环绕在头上以用于除 Enum.reduce/3 数字以外的用途。 如果您发现不变代码非常明显,请随时跳过接下来的几段。

Let’s look at this code in detail.

让我们详细看一下这段代码。

Starting with the outside reduction, we’re reducing on true, and we can simplify a bit… let’s say I wanted to establish that every integer in a list of integers was greater than 10. I could write something like this:

从外部减少开始,我们减少true ,我们可以简化一下……假设我要确定整数列表中的每个整数都大于10。我可以这样写:

small_integers = Enum.filter(list, fn i -> i <= 10 end)
less_than_tens = Enum.empty?(small_integers)

and less_than_tens would be either true or false. Or I could write this:

less_than_tens为true或false。 或者我可以这样写:

less_than_tens = 
Enum.reduce(list, true, fn i, acc -> i <= 10 && acc end)

Where once some integer has flipped the accumulator from true to false, it will remain false for the rest of the list.

一旦某个整数将累加器从“真”翻转为“假”,其余的列表将保持“假”。

The outside reduction does just that, we look at each item in the input list, and check to see whether it is in a partition. If it is not, the value in_a_list && acc_a will become false, and it stays false for the remainder of the list.

外部约简就是这样做的,我们查看输入列表中的每个项目,然后检查它是否在分区中。 如果不是,则值in_a_list && acc_a将变为false,并且在列表的其余部分保持为false。

What about finding the value of in_a_list? In this case, I have a list of lists, and I want to see if some value is in one of those lists. There are a lot of ways to do this as well. For example, I could write:

如何查找in_a_list的值? 在这种情况下,我有一个列表列表,我想看看这些列表之一中是否有值。 也有很多方法可以做到这一点。 例如,我可以写:

(i is some integer we're checking)
lists_of_bools =
Enum.map(list_of_lists, fn list -> Enum.member?(list, i))
in_a_list = Enum.member?(list_of_bools, true)

If true is a member of our list of bools, than we found i in some list.

如果true是我们的bool列表的成员,那么我们在某些列表中会找到i

Alternately, I can use a reduction:

或者,我可以使用减少量:

in_a_list = 
Enum.reduce(list_of_lists, false, fn list, acc ->
Enum.member?(list, i) || acc
end)

In this case, because of the || statement, once the item is found in a list, the accumulator becomes true, and it will remain true even if the item is not found in any subsequent list.

在这种情况下,由于|| 语句,一旦在列表中找到该项目,则累加器为true,即使在后续列表中未找到该项目,累加器也将保持为true。

End of Warning Section.

警告部分结束。

So our outer reduction looks at each item in the input list, and if every item is found in any partition (by the inner reduction) it will return true as it’s accumulated value. If any item is not found, we return false and the invariant will fail. Notice, this invariant would pass under a number of bad implementations. For example, if partiphify had the following implementation:

因此,我们的外部约简着眼于输入列表中的每一项,如果在任何分区(通过内部约简)中找到了每一项,则其累加值将返回true 。 如果未找到任何项目,则返回false ,并且不变式将失败。 注意,这个不变式将在许多错误的实现下通过。 例如,如果partiphify具有以下实现:

def partiphify!(list, k), do: [[list]]

Then this invariant would pass. Similarly, if the implementation were:

然后,该不变式将通过。 同样,如果实现是:

def partiphify!(list, k) do: (1..k) |> Enum.map(fn _i -> [] end)

Then the first invariant would pass.

然后第一个不变量将通过。

def partiphify!(list, k) do
emptys = (2..k) |> Enum.map(fn _i -> [] end)
[list] ++ emptys
end

Would pass both of these property tests, and the third invariant as well. The only invariant it would fail would be the fourth one, because the differences in sizes between partitions would exceed 1. I could pass that invariant and the 1st and 2nd invariants with this code:

将同时通过这两个属性测试以及第三个不变式。 它将失败的唯一不变式将是第四个不变式,因为分区之间的大小差异将超过1。我可以使用以下代码传递该不变式以及第一个和第二个不变式:

def partiphify!(list, k), do: (1..k) |> Enum.map(fn _i -> [list] end)

Now all the partitions are the same size, and I have the right number of partitions, and every item in the input list is in some partition. But I’ll fail the third invariant, because I’ll have k * Enum.count(list) items if I add up the count of the partitions, and I only want Enum.count(list) items.

现在所有分区的大小都相同,我拥有正确数量的分区,并且输入列表中的每个项目都在某个分区中。 但是我k * Enum.count(list)第三个不变式失败,因为如果我将分区的数量加起来,我将拥有k * Enum.count(list)项目,而我只想要Enum.count(list)项目。

Each invariant limits the implementation in different ways, and each of them limits the implementation in ways that are relevant to the function. Contrast this to our DocTest:

每个不变量以不同的方式限制实现,每个不变量以与功能相关的方式限制实现。 将此与我们的DocTest进行对比:

iex> Morphix.partiphify!([:a, :b, :c, :d, :e], 4)
[[:b], [:c], [:d], [:e, :a]]

The DocTest gives an output example that meets all four invariants, but there are a lot of irrelevant factors in it as well. It tests ordering, and grouping, and the ordering inside groupings. We don’t care about any of those things, and there are many correct implementations of the method that would fail this test. Which means that rather than being a help to refactoring, this test stands in the way of refactoring!

DocTest给出了一个满足所有四个不变式的输出示例,但是其中也有很多不相关的因素。 它测试排序和分组,以及分组内部的排序。 我们不在乎这些事情,并且有许多正确的方法实现会导致测试失败。 这意味着该测试不是重构的帮助,而是重构的方式!

An interesting question is whether we could write better unit tests around this code that would help us refactor. And of course, we could test these properties without using a property test library. We could even add some randomness to the test.

一个有趣的问题是,我们是否可以围绕该代码编写更好的单元测试,以帮助我们进行重构。 当然,我们可以在不使用属性测试库的情况下测试这些属性。 我们甚至可以为测试添加一些随机性。

test "the number of partitions should be correct" do
list = [1,2,3,4,5,6,7,8,9,10]
k = (1..27) |> Enum.random()
parts = Morphix.partiphify!(list, k)
assert Enum.count(parts) == k
end

With a little work, I could (and might) generate random lists as well. I could even change the implementation to go through every number from 1 to 27 against every one of a number of randomly generated lists, or hit some set of numbers I though was import (1, the list count / k, list count-1, stuff like that). Once I built those cases for number of partitions, it would be a simple matter to test the other invariants.

只需做一些工作,我就可以(并且可能)生成随机列表。 我什至可以更改实现,以针对随机生成的多个列表中的每个列表,从1到27遍历每个数字,或者击中一些我虽然已导入的数字(1,列表计数/ k,列表计数-1,类似的东西)。 一旦为分区数量建立了这些案例,测试其他不变式就很简单了。

These would be better Unit Tests. They might not make great DocTests, but they would be much better unit tests than testing that the input exactly matches the output. A good DocTest should be a simple example that tells people what will happen if they use the function being documented. In Elixir, it’s there to clarify and support your documentation. It’s not there to test your code.

这些将是更好的单元测试。 它们可能不是很好的DocTest,但是比测试输入与输出完全匹配的单元测试要好得多。 一个好的DocTest应该是一个简单的示例,它告诉人们如果使用所记录的功能会发生什么。 在Elixir中,可以澄清和支持您的文档。 它不在那里测试您的代码。

A good Unit Test is there to test your code. So the goals of the two tests are not the same. Something I’ve often heard and have sometimes said is that “If your tests stand in the way of refactoring, they probably aren’t very good.” You’ve probably also heard “Don’t test the implementation” when writing unit tests. But what does the second statement mean, and how do we avoid having tests stand in the way of implementation?

一个好的单元测试可以测试您的代码。 因此,这两个测试的目标并不相同。 我经常听到并且有时说的是,“如果您的测试阻碍了重构,那么它们可能不是很好。” 在编写单元测试时,您可能还会听到“不要测试实现”。 但是第二条语句是什么意思,以及如何避免测试阻碍实现?

I think the answer is this: “Try to think in properties, not examples”. That doesn’t mean adding a property testing library to your code. It means understanding what your invariants are for a piece of code, and attempting to test those invariants. It means not testing things that are not invariants as well!

我认为答案是这样的: “尝试考虑属性,而不是示例” 。 这并不意味着向代码中添加属性测试库。 这意味着了解你的变量是什么一段代码,并试图测试这些变量。 这意味着也不要测试不是不变的东西!

.If you can think in properties, with or without a property testing library, your tests will support refactoring rather than being a barrier to it.

如果您可以考虑属性,无论是否具有属性测试库,那么您的测试都将支持重构,而不是阻碍重构。

翻译自: https://medium.com/perplexinomicon-of-philosodad/preparing-to-refactor-with-property-tests-519d6b3c63bf

ios 对属性重构

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值